Compare commits

...

286 Commits

Author SHA1 Message Date
schlagmichdoch
86e0d97afb increase version to v1.9.4 2023-10-23 19:46:20 +02:00
schlagmichdoch
433373bad1 Do not inverse QR-code colors on dark mode to fix readability with some readers and the native camera app on Android (fixes #187) 2023-10-23 19:39:33 +02:00
schlagmichdoch
9a7e621af6 fix ws-fallback. Variable that indicates WebRTC support was always false 2023-10-23 19:30:13 +02:00
schlagmichdoch
46f33f894b increase version to v1.9.3 2023-10-20 20:24:53 +02:00
schlagmichdoch
84986e4eef try the generalized version of a language code before resorting to the default language (es-US -> es) 2023-10-20 19:27:15 +02:00
schlagmichdoch
dd31f375a4 increase version to v1.9.2 2023-10-20 17:44:58 +02:00
schlagmichdoch
71603fb871 Merge new translations added via Weblate 2023-10-20 17:41:28 +02:00
schlagmichdoch
2a599f6448 Add new translations to websocket fallback version 2023-10-20 17:41:11 +02:00
schlagmichdoch
9be9398dfa Merge pull request #179 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-10-20 17:36:07 +02:00
schlagmichdoch
881170af06 add forceFetch variable to service-worker.js for development 2023-10-20 17:32:16 +02:00
schlagmichdoch
28c7fcd7a2 fix header not showing on snapdrop-android app 2023-10-20 17:25:15 +02:00
kek
1ca7a201ee Translated using Weblate (Russian)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-10-18 04:00:40 +00:00
awashingmachine
80892fa6f0 Translated using Weblate (Italian)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/it/
2023-10-16 08:02:06 +00:00
Ian Perdiansah
d1be04f6ec Translated using Weblate (Indonesian)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/id/
2023-10-16 08:02:05 +00:00
mere
b9d2ff7a2a Translated using Weblate (Romanian)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ro/
2023-10-16 08:02:04 +00:00
schlagmichdoch
5b2bebcdee fix modes for preload link and fetching request do not match 2023-10-13 23:07:35 +02:00
schlagmichdoch
13d5c01935 increase version to v1.9.1 2023-10-13 18:49:19 +02:00
schlagmichdoch
787cd8dab5 Include header buttons installation and edit-paired-devices to evaluation before fading in header 2023-10-13 18:46:39 +02:00
schlagmichdoch
2131307fc6 Fix offline capability: Cache first -> fetch from network second. From now on, updates are only possible by increasing cacheVersion 2023-10-13 18:46:22 +02:00
RintanBroadleaf
0c2b9df8ee Translated using Weblate (Japanese)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ja/
2023-10-13 18:01:23 +02:00
gallegonovato
1a8bcd1b19 Translated using Weblate (Spanish)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/es/
2023-10-13 18:01:23 +02:00
Eric
00e919d132 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-10-13 18:01:22 +02:00
schlagmichdoch
9eeb710deb Translated using Weblate (German)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-10-13 18:01:22 +02:00
schlagmichdoch
3506524041 Translated using Weblate (English)
Currently translated at 100.0% (147 of 147 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-10-13 18:01:22 +02:00
schlagmichdoch
8eb6e5bfab increase version to v1.9.0 2023-10-12 13:27:41 +02:00
schlagmichdoch
bb1468fa42 Implement click on QR-code to copy room or pair link (#174) 2023-10-12 13:20:58 +02:00
schlagmichdoch
a32b310bf0 add default English translation file as preload link 2023-10-12 04:27:59 +02:00
schlagmichdoch
ed2f1b0c61 Speed up initial load while still preventing css layout shift; Tidy up ui.js 2023-10-12 04:27:56 +02:00
schlagmichdoch
2578803a78 Fix header button margin on ltr languages 2023-10-12 03:42:48 +02:00
schlagmichdoch
e367ca9f78 Remove css nesting to support devices with iOS 16.4 and older and other browsers (https://caniuse.com/css-nesting) 2023-10-12 03:42:48 +02:00
schlagmichdoch
684fe7142d Make "Connecting..." toast persistent until connection is established 2023-10-12 03:42:48 +02:00
schlagmichdoch
4e00e5b358 Add notifications-permissions-error to translations 2023-10-12 01:53:31 +02:00
dependabot[bot]
ca8a5de47e Bump express-rate-limit from 7.1.0 to 7.1.1
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 7.1.0 to 7.1.1.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v7.1.0...v7.1.1)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 23:31:18 +02:00
schlagmichdoch
ceec9c5f49 Revert incorrect changes when removing all strings from index.html 2023-10-11 23:31:18 +02:00
schlagmichdoch
81d85d0cc6 Add title of the send message text field to translations 2023-10-11 23:31:18 +02:00
schlagmichdoch
cd669b13d9 Fix display name not being editable on some older iOS devices (#163) 2023-10-11 23:31:18 +02:00
schlagmichdoch
38a80d2d34 Revert "fix textarea for sending messages is not editable on some iOS devices (#163)"
This reverts commit 1669fe7d75.
2023-10-11 23:31:18 +02:00
schlagmichdoch
d46f57d8ec Add translation files to PWA offline cache (#164) 2023-10-11 23:31:18 +02:00
schlagmichdoch
f75e6e85b2 Redraw background when language is changed 2023-10-11 23:31:18 +02:00
schlagmichdoch
7a299a5c98 Make Set Language Dialog properly scrollable on mobile devices by using the styling of Pair Device and Public Room Dialogs. 2023-10-11 23:28:56 +02:00
schlagmichdoch
4854c816a4 Enable Arabic, Dutch, Italian, Japanese and Spanish translations 2023-10-11 18:04:00 +02:00
schlagmichdoch
117a4d3eb9 adjustments to add right-to-left language 2023-10-11 18:04:00 +02:00
schlagmichdoch
8d0e560b1d Add new translations to websocket fallback version 2023-10-11 18:04:00 +02:00
schlagmichdoch
671081b86d Translated using Weblate (Japanese)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ja/
2023-10-11 18:04:00 +02:00
RintanBroadleaf
c677ac7ebe Translated using Weblate (Japanese)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ja/
2023-10-11 18:04:00 +02:00
awashingmachine
4722432209 Translated using Weblate (Italian)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/it/
2023-10-11 18:04:00 +02:00
schlagmichdoch
3366fde1fe Added translation using Weblate (Japanese) 2023-10-11 18:04:00 +02:00
awashingmachine
72071c3780 Translated using Weblate (Italian)
Currently translated at 37.5% (53 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/it/
2023-10-11 18:04:00 +02:00
Éric Gaspar
b0265efa03 Translated using Weblate (French)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/fr/
2023-10-11 18:04:00 +02:00
AkkoYK
97100ac051 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-10-11 18:04:00 +02:00
schlagmichdoch
1cce04664e Added translation using Weblate (Italian) 2023-10-11 18:04:00 +02:00
Arne Cuperus
aadda018a1 Translated using Weblate (Dutch)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nl/
2023-10-11 18:04:00 +02:00
schlagmichdoch
32aa74c1c8 Translated using Weblate (Arabic)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ar/
2023-10-11 18:04:00 +02:00
Ian Perdiansah
414048e8a1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/id/
2023-10-11 18:04:00 +02:00
schlagmichdoch
9d173d7a2c Translated using Weblate (Indonesian)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/id/
2023-10-11 18:03:59 +02:00
schlagmichdoch
9c2a076f2b Translated using Weblate (Romanian)
Currently translated at 97.8% (138 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ro/
2023-10-11 18:03:59 +02:00
kek
67f6580d70 Translated using Weblate (Russian)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-10-11 18:03:59 +02:00
schlagmichdoch
52e6ae9134 Translated using Weblate (German)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-10-11 18:03:59 +02:00
Arne Cuperus
e9664bed76 Translated using Weblate (Dutch)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nl/
2023-10-11 18:03:59 +02:00
schlagmichdoch
5b408bc6a0 Translated using Weblate (Spanish)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/es/
2023-10-11 18:03:59 +02:00
schlagmichdoch
b9476975dc Translated using Weblate (Dutch)
Currently translated at 94.3% (133 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nl/
2023-10-11 18:03:59 +02:00
Arne Cuperus
efd0b6f34f Translated using Weblate (Dutch)
Currently translated at 94.3% (133 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nl/
2023-10-11 18:03:59 +02:00
schlagmichdoch
866a1c4100 Translated using Weblate (French)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/fr/
2023-10-11 18:03:59 +02:00
schlagmichdoch
a4b905ba14 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.5% (139 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-10-11 18:03:59 +02:00
schlagmichdoch
55258d0f93 Translated using Weblate (Russian)
Currently translated at 97.8% (138 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-10-11 18:03:59 +02:00
schlagmichdoch
784c1e88db Translated using Weblate (German)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-10-11 18:03:59 +02:00
schlagmichdoch
69d28ef158 Translated using Weblate (English)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-10-11 18:03:59 +02:00
schlagmichdoch
22cf10da0a Translated using Weblate (Spanish)
Currently translated at 2.8% (4 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/es/
2023-10-11 18:03:59 +02:00
Arne Cuperus
31f1abe8a0 Translated using Weblate (Dutch)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nl/
2023-10-11 18:03:59 +02:00
schlagmichdoch
5e91bd84dc Translated using Weblate (French)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/fr/
2023-10-11 18:03:59 +02:00
schlagmichdoch
aec3d358d5 Translated using Weblate (Arabic)
Currently translated at 97.8% (138 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ar/
2023-10-11 18:03:59 +02:00
bsam bu salh
dc07b04b45 Translated using Weblate (Arabic)
Currently translated at 97.8% (138 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ar/
2023-10-11 18:03:59 +02:00
schlagmichdoch
08e3055917 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.7% (97 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
2023-10-11 18:03:59 +02:00
schlagmichdoch
88d6efc5da Translated using Weblate (German)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-10-11 18:03:59 +02:00
schlagmichdoch
bfa4662079 Translated using Weblate (English)
Currently translated at 100.0% (141 of 141 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-10-11 18:03:59 +02:00
schlagmichdoch
b1252bd184 Added translation using Weblate (Spanish) 2023-10-11 18:03:59 +02:00
schlagmichdoch
bd5a2cd862 Added translation using Weblate (Dutch) 2023-10-11 18:03:59 +02:00
Mustafa Hamed Kathem
86b08cd93f Translated using Weblate (Arabic)
Currently translated at 99.2% (139 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ar/
2023-10-11 18:03:59 +02:00
schlagmichdoch
1fc303dad2 Added translation using Weblate (Arabic) 2023-10-11 18:03:59 +02:00
schlagmichdoch
a2f2774ca0 remove all translated strings from index.html 2023-10-11 18:03:49 +02:00
schlagmichdoch
cffc2a834f increase version to v1.8.3 2023-10-09 20:53:12 +02:00
dependabot[bot]
9f8acd6b6a Bump express-rate-limit from 7.0.1 to 7.1.0
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v7.0.1...v7.1.0)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-09 20:51:21 +02:00
dependabot[bot]
60c699f3b2 Bump ws from 8.14.1 to 8.14.2
Bumps [ws](https://github.com/websockets/ws) from 8.14.1 to 8.14.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.14.1...8.14.2)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-09 20:50:58 +02:00
schlagmichdoch
1669fe7d75 fix textarea for sending messages is not editable on some iOS devices (#163) 2023-10-09 19:16:21 +02:00
schlagmichdoch
3c1ef57740 prevent Cumulative Layout Shift by fading in elements after initial translation is loaded 2023-10-06 02:57:46 +02:00
schlagmichdoch
faa2030f0b Merge pull request #166 from jimmyGALLAND/fix_dialog_transfert_request_title
fix dialog transfert request title
2023-10-03 23:12:08 +02:00
schlagmichdoch
34c5392a66 Merge pull request #165 from jimmyGALLAND/translate_fr
add french translate
2023-10-03 18:51:40 +02:00
Jimmy GALLAND
d082a885c4 add french translate
Co-authored-by: schlagmichdoch <schlagmichdoch@users.noreply.github.com>
2023-10-01 17:14:39 +02:00
Jimmy GALLAND
fd89aca219 fix transfer request dialog title
Co-authored-by: schlagmichdoch <schlagmichdoch@users.noreply.github.com>
2023-10-01 16:56:13 +02:00
schlagmichdoch
82a46ebb84 increase version to v1.8.2 2023-09-24 22:30:32 +02:00
schlagmichdoch
d7e25dc2d4 Activate Romanian and Indonesian translations 2023-09-22 02:07:45 +02:00
schlagmichdoch
eeccf78fa6 Add new translations to websocket fallback version 2023-09-22 01:54:55 +02:00
schlagmichdoch
d8043bd4e5 Merge pull request #159 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-09-22 01:52:20 +02:00
Ian Perdiansah
e6b10bc9cb Translated using Weblate (Indonesian)
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/id/
2023-09-21 20:22:24 +02:00
mere
ab4ac5c217 Translated using Weblate (Romanian)
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ro/
2023-09-21 20:22:23 +02:00
schlagmichdoch
c3c3e2685f Added translation using Weblate (Indonesian) 2023-09-21 14:26:24 +02:00
mere
a120d7c1fc Translated using Weblate (Romanian)
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ro/
2023-09-20 21:22:52 +02:00
Jiacheng Hou
ce9748fd0e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-09-20 21:22:51 +02:00
kek
d4ff69d215 Translated using Weblate (Russian)
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-09-20 21:22:51 +02:00
schlagmichdoch
fdf9457a0d Translated using Weblate (German)
Currently translated at 100.0% (140 of 140 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-20 21:22:51 +02:00
schlagmichdoch
b537541788 Added translation using Weblate (Romanian) 2023-09-20 16:22:25 +02:00
kek
49254bcca9 Translated using Weblate (Russian)
Currently translated at 100.0% (139 of 139 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-09-20 02:14:10 +02:00
schlagmichdoch
9a7ee95d2f Add missing strings form public room dialog to translation files - second try 2023-09-20 02:13:22 +02:00
schlagmichdoch
e0548102d3 increase version to v1.8.1 2023-09-19 18:05:54 +02:00
schlagmichdoch
d7626822fa Merge missing translations from branch 'translate' 2023-09-19 18:03:13 +02:00
schlagmichdoch
426b75f336 Add new translations to websocket fallback version 2023-09-19 18:02:13 +02:00
schlagmichdoch
f63aebbcb4 Translated using Weblate (German)
Currently translated at 100.0% (139 of 139 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-19 17:58:32 +02:00
schlagmichdoch
5e74f515a7 Translated using Weblate (English)
Currently translated at 100.0% (139 of 139 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-09-19 17:58:32 +02:00
schlagmichdoch
e807c3959a fix #157: catch EADDRINUSE to prevent endless restarts if the port is already in use 2023-09-19 17:53:21 +02:00
schlagmichdoch
d77ccdf233 fix TypeError 2023-09-19 17:50:16 +02:00
schlagmichdoch
d45c6b5bea increase margin for hr-note ("OR") on dialogs 2023-09-19 00:24:32 +02:00
schlagmichdoch
5625bf6f93 Add missing strings form public room dialog to translation files 2023-09-19 00:23:19 +02:00
schlagmichdoch
efec6eaa1a increase version to v1.8.0 2023-09-18 22:52:48 +02:00
schlagmichdoch
2abbfcea75 Fix spelling in German language files 2023-09-18 22:52:10 +02:00
schlagmichdoch
b7abc1d174 Merge pull request #146 from schlagmichdoch/dependabot/npm_and_yarn/ua-parser-js-1.0.36
Bump ua-parser-js from 1.0.35 to 1.0.36
2023-09-18 22:40:33 +02:00
dependabot[bot]
71d92bca71 Bump ua-parser-js from 1.0.35 to 1.0.36
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.35 to 1.0.36.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/commits)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 20:39:57 +00:00
schlagmichdoch
b4f9f9b048 Merge pull request #145 from schlagmichdoch/dependabot/npm_and_yarn/ws-8.14.1
Bump ws from 8.13.0 to 8.14.1
2023-09-18 22:39:06 +02:00
schlagmichdoch
6b98d84697 Merge pull request #155 from schlagmichdoch/dependabot/npm_and_yarn/express-rate-limit-7.0.1
Bump express-rate-limit from 6.9.0 to 7.0.1
2023-09-18 22:38:56 +02:00
schlagmichdoch
c305996799 Merge pull request #151 from schlagmichdoch/translate
Add two major features: Translation #35 & Temporary Public Room  #55
2023-09-18 22:38:24 +02:00
schlagmichdoch
684e43a352 prevent autofocus of dialog elements on mobile 2023-09-18 22:32:21 +02:00
schlagmichdoch
fc190dd121 fix discovery-wrapper 2023-09-18 22:13:30 +02:00
schlagmichdoch
6c07dc3678 Fix translation and design of base64dialog 2023-09-18 22:03:18 +02:00
schlagmichdoch
dbe3381e7d Fix concatenation of translated strings in paste mode 2023-09-18 21:27:56 +02:00
schlagmichdoch
0bae7c84bd Enable German translation 2023-09-18 21:27:48 +02:00
schlagmichdoch
ddd5c7df8c Remove deleted translation strings and-by and discovery-everyone from all translation files 2023-09-18 18:17:16 +02:00
schlagmichdoch
33a6a1b8dc Add new translations to websocket fallback version 2023-09-18 18:11:31 +02:00
schlagmichdoch
4c1de16eb6 Merge pull request #139 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-09-18 18:08:32 +02:00
dependabot[bot]
00b95ed16a Bump express-rate-limit from 6.9.0 to 7.0.1
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.9.0 to 7.0.1.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.9.0...v7.0.1)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 04:35:31 +00:00
AkkoYK
e7e3221e4c Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-09-17 13:59:53 +00:00
kek
c7f4166f0b Translated using Weblate (Russian)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-09-17 13:59:53 +00:00
schlagmichdoch
292a674482 Translated using Weblate (German)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-17 13:59:52 +00:00
schlagmichdoch
95231d0507 Translated using Weblate (German)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
schlagmichdoch
c7263c0aa4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 71.3% (97 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
2023-09-15 12:39:40 +02:00
schlagmichdoch
09583ae6c8 Translated using Weblate (German)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
ann0see
b584b6adc1 Translated using Weblate (German)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
schlagmichdoch
3355351c98 Translated using Weblate (English)
Currently translated at 100.0% (136 of 136 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-09-15 12:39:40 +02:00
schlagmichdoch
3c7218e2d5 Translated using Weblate (Chinese (Simplified))
Currently translated at 83.7% (113 of 135 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-09-15 12:39:40 +02:00
schlagmichdoch
8caec76ba0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.6% (94 of 135 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
2023-09-15 12:39:40 +02:00
schlagmichdoch
4ad54c6023 Translated using Weblate (Russian)
Currently translated at 83.7% (113 of 135 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-09-15 12:39:40 +02:00
schlagmichdoch
0e9df8cc17 Translated using Weblate (German)
Currently translated at 85.9% (116 of 135 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
schlagmichdoch
7e63994140 Translated using Weblate (English)
Currently translated at 100.0% (135 of 135 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-09-15 12:39:40 +02:00
ann0see
f92c7acdce Translated using Weblate (German)
Currently translated at 98.4% (123 of 125 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
schlagmichdoch
a51255b84a Translated using Weblate (English)
Currently translated at 100.0% (125 of 125 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-09-15 12:39:40 +02:00
schlagmichdoch
2985bc0d08 Translated using Weblate (German)
Currently translated at 89.6% (112 of 125 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
ann0see
e34556bc74 Translated using Weblate (German)
Currently translated at 89.6% (112 of 125 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
kek
16fd866445 Translated using Weblate (Russian)
Currently translated at 100.0% (122 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-09-15 12:39:40 +02:00
ann0see
df734a4fc7 Translated using Weblate (German)
Currently translated at 67.2% (82 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
ann0see
e9b1c51530 Translated using Weblate (German)
Currently translated at 19.6% (24 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
Ettore Atalan
c7357e7ecc Translated using Weblate (German)
Currently translated at 1.6% (2 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
2023-09-15 12:39:40 +02:00
schlagmichdoch
446035d704 Deleted translation using Weblate (Chinese (Simplified, Singapore)) 2023-09-15 12:39:40 +02:00
schlagmichdoch
6492f84c4e Added translation using Weblate (Chinese (Simplified, Singapore)) 2023-09-15 12:39:40 +02:00
schlagmichdoch
3f1477f24a Fix public room badge not translating correctly when switching languages 2023-09-14 20:23:04 +02:00
schlagmichdoch
a314b2c275 remove no longer needed css var --footer-height; Fix overlaying of websocket fallback notification 2023-09-14 19:03:43 +02:00
schlagmichdoch
3faa173dde Make "Unpair" and "auto-accept" translatable on the Edit Paired Devices Dialog 2023-09-14 18:15:07 +02:00
schlagmichdoch
8b30940056 Add translation and temporary public rooms to README 2023-09-14 17:27:51 +02:00
schlagmichdoch
de49af3bca apply all changes to ws-fallback 2023-09-14 15:45:40 +02:00
schlagmichdoch
8d2584fa69 implement temporary public rooms, tidy up index.js, rework UI dialogs and change colors slightly 2023-09-14 15:44:28 +02:00
schlagmichdoch
bd7b3c6d28 show warning to user if navigator.clipboard.writeText fails 2023-09-13 18:33:17 +02:00
schlagmichdoch
c71bf456e3 fix "and 2 other files" div not cleared properly 2023-09-13 18:33:17 +02:00
schlagmichdoch
6679ef7529 fix keepAliveTimers not correctly cleared on reconnect 2023-09-13 18:33:17 +02:00
schlagmichdoch
02911804cb add default values to Localization.getTranslation function 2023-09-13 18:33:17 +02:00
schlagmichdoch
d689fe28e5 add English language names next to native language names to language select dialog 2023-09-13 18:32:57 +02:00
dependabot[bot]
1c946fc78e Bump ws from 8.13.0 to 8.14.1
Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.14.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.14.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 04:32:13 +00:00
schlagmichdoch
17afa18d84 add translation selector and fix translation of data-attributes 2023-08-30 14:57:40 +02:00
schlagmichdoch
4510ba5164 Merge pull request #140 from meichthys/patch-1
Add alternate TURN server option to config notes
2023-08-29 19:03:00 +02:00
MeIchthys
d738258869 Add alternate TURN server option
Added note about using an alternate TURN server like OpenRelay.
2023-08-29 09:10:05 -04:00
schlagmichdoch
19f56a8499 enable Norwegian, Russian, and Chinese 2023-08-29 02:33:54 +02:00
schlagmichdoch
abc06fcc21 fix translation fallback for sparely translated languages when complete categories are missing 2023-08-29 02:33:12 +02:00
schlagmichdoch
c2a746d69c fix html attribute translation 2023-08-29 02:30:01 +02:00
schlagmichdoch
72f0aff60e copy lang files to ws_fallback version 2023-08-29 01:49:32 +02:00
schlagmichdoch
161bd2be84 rename language files to map language codes properly 2023-08-29 01:49:09 +02:00
schlagmichdoch
c36c8dae59 Merge branch 'master' into translate 2023-08-28 15:05:11 +02:00
schlagmichdoch
22bf1be2b7 Add TLS requirement to host-your-own.md 2023-08-28 13:41:12 +02:00
schlagmichdoch
6db844565e Merge pull request #123 from comradekingu/patch-1
Documentation language reworked
2023-08-28 13:29:56 +02:00
Allan Nordhøy
efeff84320 "HEALTHCHECK" 2023-08-11 18:59:26 +00:00
schlagmichdoch
e0210b0307 Rename enhancement to enhancement.md 2023-08-11 13:21:10 +02:00
schlagmichdoch
f95181c057 Create enhancement issue template 2023-08-11 13:20:38 +02:00
schlagmichdoch
69f1688dfe Update bug-report.md 2023-08-11 13:11:51 +02:00
schlagmichdoch
43824d0de2 increase version to v1.7.7 2023-08-10 17:09:51 +02:00
schlagmichdoch
2efb531765 Merge pull request #138
Bump express-rate-limit from 6.8.0 to 6.9.0
2023-08-10 17:06:22 +02:00
schlagmichdoch
d9686a6706 Merge pull request #137 from Zhongbing-Chen/master
revise the command line tool
2023-08-10 16:31:48 +02:00
dependabot[bot]
395c3e00a4 Bump express-rate-limit from 6.8.0 to 6.9.0
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.8.0...v6.9.0)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-07 04:24:11 +00:00
zhongbing
8869c3c27e revise the command line tool 2023-08-06 00:47:01 +08:00
schlagmichdoch
30a1c72a0c Add Weblate and a mention of translations to the README 2023-07-30 17:56:07 +02:00
schlagmichdoch
3c56427bf7 Merge pull request #126 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-07-30 17:49:55 +02:00
schlagmichdoch
d252900055 Merge branch 'translate' into weblate-pairdrop-pairdrop-spa 2023-07-30 17:48:50 +02:00
schlagmichdoch
da5038a51a include translations for about buttons and implement translation fallback if used translation is not complete 2023-07-30 17:42:46 +02:00
Kyle
b319fbe156 Translated using Weblate (Turkish)
Currently translated at 12.2% (15 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/tr/
2023-07-27 12:04:03 +02:00
schlagmichdoch
b07b8316ff Merge pull request #129 from schlagmichdoch/dependabot/npm_and_yarn/express-rate-limit-6.8.0
Bump express-rate-limit from 6.7.0 to 6.8.0
2023-07-25 18:58:11 +02:00
schlagmichdoch
714608ce97 Added translation using Weblate (Turkish) 2023-07-25 18:56:56 +02:00
Allan Nordhøy
471278f7b0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.9% (100 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
2023-07-24 18:07:00 +02:00
dependabot[bot]
445a295404 Bump express-rate-limit from 6.7.0 to 6.8.0
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.7.0 to 6.8.0.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.7.0...v6.8.0)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 04:58:27 +00:00
伍建兴
6d7c13775f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (122 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/
2023-07-21 05:09:24 +02:00
schlagmichdoch
dccc17400c Added translation using Weblate (Chinese (Simplified)) 2023-07-19 10:50:01 +02:00
Allan Nordhøy
6563ec98b3 Bold button scheme 2023-07-08 13:26:48 +00:00
Allan Nordhøy
17a12baa2a technical-documentation reworked 2023-07-08 13:24:42 +00:00
Allan Nordhøy
913b60b712 how-to reworked 2023-07-08 13:17:35 +00:00
Allan Nordhøy
9424f704bf host-your-own reworked 2023-07-08 13:14:07 +00:00
Allan Nordhøy
044d7aa20d docker-swarm-usage reworked 2023-07-08 12:59:58 +00:00
Allan Nordhøy
9b71d93dd3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 80.3% (98 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
2023-07-08 02:29:41 +02:00
kek
65ec416646 Translated using Weblate (Russian)
Currently translated at 100.0% (122 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-07-08 02:29:40 +02:00
Allan Nordhøy
99faa6bbfd Translated using Weblate (English)
Currently translated at 100.0% (122 of 122 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/
2023-07-08 02:29:40 +02:00
Allan Nordhøy
525fd295b7 Added translation using Weblate (Norwegian Bokmål) 2023-07-07 16:08:09 +02:00
kek
410936dcd8 Translated using Weblate (Russian)
Currently translated at 100.0% (118 of 118 strings)

Translation: PairDrop/pairdrop-spa
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/
2023-07-07 15:18:54 +02:00
schlagmichdoch
1d333c850c Added translation using Weblate (Russian) 2023-07-07 15:18:54 +02:00
schlagmichdoch
ebde466019 Added translation using Weblate (German) 2023-07-07 15:18:54 +02:00
schlagmichdoch
ba46befde4 include translations for about buttons and implement translation fallback if used translation is not complete 2023-07-07 15:17:42 +02:00
schlagmichdoch
f50d7438b6 implement localization 2023-07-06 21:34:21 +02:00
Allan Nordhøy
2d0ea9a2f1 Update FAQ reworked 2023-06-21 22:55:42 +00:00
schlagmichdoch
29b91cb17a increase version to v1.7.6 2023-06-01 01:51:51 +02:00
schlagmichdoch
26bf4d6dc3 ensure that otherPeers never receive peer-left after peer-joined on reconnect by leaving room before rejoining it 2023-06-01 01:49:07 +02:00
schlagmichdoch
f195c686e7 increase version to v1.7.5 2023-06-01 01:32:06 +02:00
schlagmichdoch
3505f161c6 strip 'NO-BREAK SPACE' (U+00A0) of received text as some browsers seem to add them when pasting text 2023-06-01 01:29:00 +02:00
schlagmichdoch
3e2368c0c9 stabilize connection on reconnect by terminating websocket only on timeout and not always when peer leaves its ip room 2023-06-01 01:26:53 +02:00
schlagmichdoch
d36cd3524c Fix clearBrowserHistory: url should not always be replaced by "/" as PairDrop might not always be hosted at domain root 2023-05-30 02:34:50 +02:00
schlagmichdoch
a3a8228327 increase version to v1.7.4 2023-05-26 20:37:38 +02:00
schlagmichdoch
520b772bc8 fix #112 and differentiate between textContent and innerText 2023-05-26 20:36:12 +02:00
schlagmichdoch
27bf0fa74f fix #113 2023-05-26 20:36:12 +02:00
schlagmichdoch
e9f3c39f38 Merge pull request #114 from fm-sys/patch-1
Add 'files-sent' event used by 'Snapdrop & PairDrop for Android' app to support vibrations
2023-05-26 20:35:54 +02:00
fm-sys
58b7f6bb7c Add 'files-sent' event 2023-05-26 09:52:17 +02:00
schlagmichdoch
b5987cf017 increase version to v1.7.3 2023-05-23 02:45:29 +02:00
schlagmichdoch
4433e1c58f add version number to about page 2023-05-23 02:44:25 +02:00
schlagmichdoch
b106d90b64 Fix ReferenceError: ipv6_lcl is not defined 2023-05-23 02:43:17 +02:00
schlagmichdoch
c9e1c2504a Merge pull request #110 from luckman212/luckman212-patch-1
Add new env var `IPV6_LOCALIZE` to enable auto discovery for IPv6 addresses
2023-05-23 01:53:23 +02:00
luckman212
32e909b8c2 fixes for https://github.com/schlagmichdoch/PairDrop/issues/69
(squashed, docs updated, IPV6_LOCALIZE input validation)
2023-05-19 12:18:20 -04:00
schlagmichdoch
a444be226f Fix canvas selector 2023-05-16 19:15:47 +02:00
schlagmichdoch
df778ba42c Speed up canvas by removing fade-in animation 2023-05-16 19:09:59 +02:00
schlagmichdoch
8a17b82fa4 Fix _textInputEmpty() for Chromium based browsers
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:53:56 +02:00
schlagmichdoch
56eb29c91b increase version to v1.7.2 2023-05-16 02:35:03 +02:00
schlagmichdoch
6e4bda0adf Fix message sending via submit button.
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:25:50 +02:00
Lopolin-LP
0baced640a Fix About Background not filling up full viewport under certain circumstances (#109)
* Fix About Background Not filling up full viewport under certain circumstances

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

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

---------

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

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 05:05:12 +00:00
Rob van Oostenrijk
f5fde731b0 Build multi-architecture image
Build image output both for amd64 & arm64
2023-03-31 08:37:11 +04:00
schlagmichdoch
d6eee480b3 Add darkmode/lightmode/auto toggle to "Other changes" 2023-03-29 17:10:20 +02:00
schlagmichdoch
f6ad85a744 increase version to v1.5.3 2023-03-29 16:24:24 +02:00
schlagmichdoch
d50480b2f8 Merge pull request #94 from schlagmichdoch/add_theme_menu
Add theme menu to toggle between auto, light and dark mode.
2023-03-29 16:22:42 +02:00
schlagmichdoch
ac1e88b6a0 Add possibility to reset theme to auto 2023-03-29 01:39:45 +02:00
schlagmichdoch
0fe36e132c Remove the "under development" message from the share-menu section 2023-03-28 20:24:46 +02:00
schlagmichdoch
ab08091f5d increase version to v1.5.2 2023-03-28 20:00:05 +02:00
schlagmichdoch
19a78a5239 Merge pull request #72 from schlagmichdoch/fix_share_target
WIP: Fix share target API
2023-03-28 19:58:34 +02:00
schlagmichdoch
d0b2c81582 Tidy up code 2023-03-28 19:07:33 +02:00
Daniel Pham
10a0aaf896 Fix passed arguments for sharing text 2023-03-28 19:00:15 +02:00
Daniel Pham
34ebd60304 Update service worker
- files array now matches manifest files name
- fixed handling fetch redirect
2023-03-28 19:00:15 +02:00
schlagmichdoch
251df2fbff try to fix share target api 2023-03-28 19:00:05 +02:00
schlagmichdoch
1bb8a63eed increase version to v1.5.1 2023-03-27 02:31:56 +02:00
schlagmichdoch
7d581ca858 Merge pull request #92 from schlagmichdoch/optimize_animation
Optimize background animation to drastically reduce CPU/GPU resources
2023-03-27 02:19:19 +02:00
schlagmichdoch
dcc4e8b747 Optimize background animation drastically by using offscreen canvases to reuse frames. Rewrite animate function to prevent it from being called multiple times 2023-03-27 02:17:36 +02:00
schlagmichdoch
ce549adf22 Merge pull request #82 from kgncloud/patch-1
Add Healthcheck to Dockerfile
2023-03-25 04:09:46 +01:00
schlagmichdoch
bdb39a1d2c add docker-swarm-usage.md reference to host-your-own.md and tidy up docker-swarm-usage.md 2023-03-25 04:08:11 +01:00
schlagmichdoch
195dfd0bb3 Add https requirement for PWAs to faq.md 2023-03-25 03:37:15 +01:00
Kaindl Network
680ed81bd7 Create 2023-03-18 23:52:33 +00:00
Kaindl Network
b7781e2bab Add Healthcheck to Dockerfile 2023-03-13 20:38:36 +01:00
65 changed files with 11043 additions and 2395 deletions

View File

@@ -1,8 +1,8 @@
---
name: Bug Report
about: Create a report to help us improve. Please check the FAQ first.
title: 'Bug:/Enhancement:/Feature Request: '
labels: ''
title: '[Bug] '
labels: 'bug'
assignees: ''
---
@@ -34,12 +34,17 @@ If applicable, add screenshots to help explain your problem.
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Self-Hosted**
**Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes
Version: v1.9.4
**Bug occurs on self-hosted PairDrop instance**
No | Yes
**Self-Hosted Setup**
Proxy: Nginx | Apache2
Deployment: docker run | docker-compose | npm run start:prod
Version: v1.9.4
**Additional context**
Add any other context about the problem here.

20
.github/ISSUE_TEMPLATE/enhancement.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Enhancement
about: Enhancements and feature requests are always welcome. See discussions regarding central topics.
title: '[Enhancement] '
labels: 'enhancement'
assignees: ''
---
**What problem is solved by the new feature**
What's the motivation for this topic
**Describe the feature**
A clear and concise description of what the new feature/enhancement is.
**Drafts**
Screenshots of Draw.io graph or drawn sketch.
**Additional context**
Add any other context here.

View File

@@ -28,6 +28,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup qemu
uses: docker/setup-qemu-action@v2.1.0
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
@@ -46,6 +52,7 @@ jobs:
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -9,3 +9,6 @@ RUN npm ci
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1

View File

@@ -20,8 +20,12 @@
## Features
[PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
Send images, documents or text via peer to peer connection to devices in the same local network/Wi-Fi or to paired devices.
As it is web based, it runs on all devices.
- File Sharing on your local network
- Send images, documents or text via peer to peer connection to devices on the same local network.
- Internet Transfers
- Join temporary public rooms to transfer files easily over the internet!
- Web-Application
- As it is web based, it runs on all devices.
You want to quickly send a file from your phone to your laptop?
<br>You want to share photos in original quality with friends that use a mixture of Android and iOS?
@@ -32,14 +36,29 @@ You want to quickly send a file from your phone to your laptop?
Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
## Differences to Snapdrop
<details><summary>Click to expand</summary>
### Device Pairing
* Pair devices via 6-digit code or QR-Code
* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
### Paired Devices and Public Rooms - Internet Transfer
* Transfer files over the internet between paired devices or by entering temporary public rooms.
* Connect to devices in complex network environments (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
* Connect to devices on your mobile hotspot.
* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App
* You will always discover devices on your local network. Paired devices are shown additionally.
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
* Devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
* Connect to devices on your mobile hotspot.
* You will always discover devices on your local network. Paired devices and devices in the same public room are shown additionally.
#### Persistent Device Pairing
* Pair your devices via a 6-digit code or a QR-Code.
* Paired devices will always find each other via shared secrets independently of their local network.
* Paired devices are persistent. You find your devices even after reopening PairDrop.
* You can edit and unpair devices easily
* Ideal to always connect easily to your own devices
#### Temporary Public Rooms
* Enter a public room via a 5-letter code or a QR-Code.
* Enter a public room to temporarily connect to devices outside your local network.
* All devices in the same public room see each other mutually.
* Public rooms are temporary. Public rooms are left as soon as PairDrop is closed.
* Ideal to connect easily to others in complex network situations or over the internet.
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
@@ -57,21 +76,21 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* Change your display name permanently to easily differentiate your devices
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
* Warn user before PairDrop is closed on file transfer
* Warn user before PairDrop is closed on file transfer
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
* [Video and Audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads))
* Switch theme back to auto/system after darkmode or lightmode is enabled
* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
* Built-in translations
</details>
## Screenshots
<div align="center">
![Pairdrop Preview](/docs/pairdrop_screenshot_mobile.gif)
</div>
<img src="https://raw.githubusercontent.com/schlagmichdoch/PairDrop/master/docs/pairdrop_screenshot_mobile.gif" style="max-height: 50vh">
## PairDrop is built with the following awesome technologies:
* Vanilla HTML5 / ES6 / CSS3 frontend
@@ -81,23 +100,29 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
* [cyrb53](https://github.com/bryc) super fast hash function
* [Weblate](https://weblate.org/) Web based localization tool
Have any questions? Read our [FAQ](/docs/faq.md).
You can [host your own instance with Docker](/docs/host-your-own.md).
## Support the Community
PairDrop is free and always will be. Still, we have to pay for the domain.
To contribute and support me:<br>
## Support PairDrop
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
</a>
PairDrop is free and always will be.
Still, we have to pay for the domain and the server.
To contribute and support, please use BuyMeACoffee via the button above.
Thanks a lot for supporting free and open software!
To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop).
## Translate PairDrop
<a href="https://hosted.weblate.org/engage/pairdrop/">
<img src="https://hosted.weblate.org/widget/pairdrop/pairdrop-spa/open-graph.png" alt="Translation status" style="max-height: 30vh" />
</a>
## How to contribute

19
docker-compose-coturn.yml Normal file
View File

@@ -0,0 +1,19 @@
version: "3"
services:
node:
image: "node:lts-alpine"
user: "node"
working_dir: /home/node/app
volumes:
- ./:/home/node/app
command: ash -c "npm i && npm run start:prod"
restart: unless-stopped
ports:
- "3000:3000"
coturn_server:
image: "coturn/coturn"
restart: always
network_mode: "host"
volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf
#you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password

View File

@@ -0,0 +1,47 @@
# Docker Swarm Usage
## Healthcheck
The [Docker Image](../Dockerfile) includes a health check with the following options:
```
--interval=30s
```
> Specifies the time interval to run the health check. \
> In this case, the health check is performed every 30 seconds.
<br>
```
--timeout=10s
```
> Specifies the amount of time to wait for a response from the \"HEALTHCHECK\" command. \
> If the response does not arrive within 10 seconds, the health check fails.
<br>
```
--start-period=5s
```
> Specifies the amount of time to wait before starting the health check process. \
> In this case, the health check process will begin 5 seconds after the container is started.
<br>
```
--retries=3
```
> Specifies the number of times Docker should retry the health check \
> before considering the container to be unhealthy.
<br>
The CMD instruction is used to define the command that will be run as part of the health check. \
In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. \
This command will attempt to connect to `http://localhost:3000/` \
and if it fails it will exit with a status code of `1`. \
If this command returns a status code other than `0`, the health check fails.
Overall, this \"HEALTHCHECK\" instruction is defining a health check process \
that runs every 30 seconds, and waits up to 10 seconds for a response, \
begins 5 seconds after the container is started, and retries up to 3 times. \
The health check attempts to connect to http://localhost:3000/ \
and will considers the container unhealthy if unable to connect.

View File

@@ -5,12 +5,20 @@
Help! I can't install the PWA!
</summary>
if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install PairDrop PWA on your desktop
if you are using a Chromium-based browser (Chrome, Edge, Vivaldi, Brave, etc.), you can easily install PairDrop PWA on your desktop
by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).
<img width="400" src="pwa-install.png" alt="Example on how to install a pwa with Edge">
On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/)
<br>
<b>Self-Hosted Instance?</b>
To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
<br>
</details>
<details>
@@ -18,11 +26,13 @@ On Firefox, PWAs are installable via [this browser extensions](https://addons.mo
Shortcuts?
</summary>
Shortcuts!
Shortcuts
- Send a message with `CTRL + ENTER`
- Close all send and pair dialogs by pressing `Escape`.
- Copy a received message to clipboard with `CTRL/⌘ + C`.
- Accept file transfer request with `Enter` and decline with `Escape`.
- Close all "Send" and "Pair" dialogs by pressing `Esc`.
- Copy a received message to the clipboard with `CTRL/⌘ + C`.
- Accept file-transfer requests with `Enter` and decline with `Esc`.
<br>
</details>
<details>
@@ -31,23 +41,25 @@ Shortcuts!
</summary>
Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.
It simply does not offer the option for images shared from a website.
It simply does not offer that option for images shared from a website.
iOS Shortcuts to the win:
iOS Shortcuts saves the day:
I created a simple iOS shortcut that takes your photos and saves them to your gallery:
https://routinehub.co/shortcut/13988/
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Is it possible to send files or text directly from the context or share menu?
Is it possible to send files or text directly from the "Context" or "Share" menu?
</summary>
Yes, it finally is!
* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
Yes, it finally is.
* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
<br>
</details>
@@ -56,53 +68,82 @@ Yes, it finally is!
Is it possible to send files or text directly via CLI?
</summary>
Yes, it is!
Yes.
* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Are there any Third-Party Apps?
Are there any third-party Apps?
</summary>
Here's a list of some third-party apps compatible with PairDrop:
These third-party apps are compatible with PairDrop:
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
3. Feel free to make one :)
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
</summary>
It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer.
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about privacy? Will files be saved on third-party-servers?
What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server?
</summary>
None of your files are ever sent to any server. Files are sent only between peers. PairDrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
WebRTC encrypts the files on transit.
It uses a WebRTC peer-to-peer connection.
WebRTC needs a signaling server that is only used to establish a connection.
The server is not involved in the file transfer.
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
If the devices are on the same network,
none of your files are ever sent to any server.
If your devices are paired and behind a NAT,
the PairDrop TURN Server is used to route your files and messages.
See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)
to learn more about STUN, TURN and WebRTC.
If you host your own instance
and want to support devices that do not support WebRTC,
you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about security? Are my files encrypted while being sent between the computers?
What about privacy? Will files be saved on third-party servers?
</summary>
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
Files are sent directly between peers.
PairDrop doesn't even use a database.
If curious, study [the server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
WebRTC encrypts the files in transit.
If the devices are on the same network,
none of your files are ever sent to any server.
If your devices are paired and behind a NAT,
the PairDrop TURN Server is used to route your files and messages.
See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)
to learn more about STUN, TURN and WebRTC.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about security? Are my files encrypted while sent between the computers?
</summary>
Yes. Your files are sent using WebRTC, encrypting them in transit.
To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack),
compare the security number shown under the device name on both devices.
The security number is different for every connection.
<br>
</details>
@@ -111,18 +152,19 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure
Transferring many files with paired devices takes too long
</summary>
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
Naturally, if traffic needs to be routed through the TURN server
because your devices are behind different NATs, transfer speed decreases.
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
You can open a hotspot on one of your devices to bridge the connection,
which omits the need of the TURN server.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
- [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot)
You can also use mobile hotspots on phones to do that.
Then, all data should be sent directly between devices and your data plan should not be charged.
Then, all data should be sent directly between devices and not use your data plan.
<br>
</details>
@@ -131,24 +173,34 @@ Then, all data should be sent directly between devices and your data plan should
Why don't you implement feature xyz?
</summary>
Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
Snapdrop and PairDrop are a study in radical simplicity.
The user interface is insanely simple.
Features are chosen very carefully because complexity grows quadratically
since every feature potentially interferes with each other feature.
We focus very narrowly on a single use case: instant file transfer.
Not facilitating optimal edge-cases means better flow for average users.
Don't be sad. We may decline your feature request for the sake of simplicity.
If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).
Read *Insanely Simple: The Obsession that Drives Apple's Success*,
and/or *Thinking, Fast and Slow* to learn more.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Snapdrop and PairDrop are awesome! How can I support them?
Snapdrop and PairDrop are awesome. How can I support them?
</summary>
* [Buy me a coffee to support open source software](https://www.buymeacoffee.com/pairdrop)
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software.
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
* Share PairDrop on social media.
* Fix bugs and make a pull request.
* Do security analysis and suggestions
* Do some security analysis and make suggestions.
* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop)
<br>
</details>
@@ -157,7 +209,9 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob
How does it work?
</summary>
[See here for Information about the Technical Implementation](/docs/technical-documentation.md)
[See here for info about the technical implementation](/docs/technical-documentation.md)
<br>
</details>
[< Back](/README.md)

View File

@@ -1,6 +1,17 @@
# Deployment Notes
The easiest way to get PairDrop up and running is by using Docker.
> <b>TURN server for Internet Transfer</b>
>
> Beware that you have to host your own TURN server to enable transfers between different networks.
>
> Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \
> or deploy it via docker-compose (Step 5).
> <b>PairDrop via HTTPS</b>
>
> On some browsers PairDrop must be served over TLS in order for some feautures to work properly. These may include copying an incoming message via the 'copy' button, installing PairDrop as PWA, persistent pairing of devices and changing of the display name, and notifications. Naturally, this is also recommended to increase security.
## Deployment with Docker
### Docker Image from Docker Hub
@@ -9,9 +20,11 @@ The easiest way to get PairDrop up and running is by using Docker.
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
> To prevent bypassing the proxy by reaching the docker container directly, \
> `127.0.0.1` is specified in the run command.
#### Options / Flags
Set options by using the following flags in the `docker run` command:
@@ -24,22 +37,39 @@ Set options by using the following flags in the `docker run` command:
> - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000`
##### Rate limiting requests
```
```bash
-e RATE_LIMIT=true
```
> Limits clients to 1000 requests per 5 min
##### IPv6 Localization
```bash
-e IPV6_LOCALIZE=4
```
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments \
> of the client IPv6 address to be evaluated as the peer's IP. \
> This can be especially useful when using Cloudflare as a proxy.
>
> The flag must be set to an **integer** between `1` and `7`. \
> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \
> to match the client IP against. The most common value would be `4`, \
> which will group peers within the same `/64` subnet.
##### Websocket Fallback (for VPN)
```bash
-e WS_FALLBACK=true
```
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
> Provides PairDrop to clients with an included websocket fallback \
> if the peer to peer WebRTC connection is not available to the client.
>
> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance.
> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
> This is not used on the official https://pairdrop.net website, \
> but you can activate it on your self-hosted instance.
> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
>
> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer!
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
> **Warning:** All traffic sent between devices using this fallback \
> is routed through the server and therefor not peer to peer! \
> Beware that the traffic routed via this fallback is readable by the server. \
> Only ever use this on instances you can trust. \
> Additionally, beware that all traffic using this fallback debits the servers data plan.
##### Specify STUN/TURN Servers
@@ -47,9 +77,13 @@ Set options by using the following flags in the `docker run` command:
-e RTC_CONFIG="rtc_config.json"
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> Specify the STUN/TURN servers PairDrop clients use by setting \
> `RTC_CONFIG` to a JSON file including the configuration. \
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
> Alternatively, use a free, pre-configured TURN server like [OpenRelay]([url](https://www.metered.ca/tools/openrelay/))
>
> Default configuration:
> ```json
> {
@@ -57,30 +91,55 @@ Set options by using the following flags in the `docker run` command:
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
##### Debug Mode
```bash
-e DEBUG_MODE="true"
```
> Use this flag to enable debugging information about the connecting peers IP addresses. \
> This is quite useful to check whether the [#HTTP-Server](#http-server) \
> is configured correctly, so the auto-discovery feature works correctly. \
> Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device, everything is set up correctly. \
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
<br>
### Docker Image from GHCR
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> You must use a server proxy to set the X-Forwarded-For to prevent \
> all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
> To prevent bypassing the proxy by reaching the Docker container directly, \
> `127.0.0.1` is specified in the run command.
>
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
> To specify options replace `npm run start:prod` \
> according to [the documentation below.](#options--flags-1)
> The Docker Image includes a healthcheck. \
> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
### Docker Image self-built
#### Build the image
@@ -95,11 +154,17 @@ docker build --pull . -f Dockerfile -t pairdrop
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
> To prevent bypassing the proxy by reaching the Docker container \
> directly, `127.0.0.1` is specified in the run command.
>
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
> To specify options replace `npm run start:prod` \
> according to [the documentation below.](#options--flags-1)
> The Docker Image includes a Healthcheck. \
Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
<br>
@@ -125,9 +190,11 @@ services:
Run the compose file with `docker compose up -d`.
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
> To prevent bypassing the proxy by reaching the Docker container \
> directly, `127.0.0.1` is specified in the run command.
<br>
@@ -153,7 +220,7 @@ or
npm start
```
> Remember to check your IP Address using your OS command to see where you can access the server.
> Remember to check your IP address using your OS command to see where you can access the server.
> By default, the node server listens on port 3000.
@@ -171,6 +238,13 @@ $env:PORT=3010; npm start
```
> Specify the port PairDrop is running on. (Default: 3000)
#### IPv6 Localization
```bash
IPV6_LOCALIZE=4
```
> Truncate a portion of the client IPv6 address to make peers more discoverable. \
> See [Options/Flags](#options--flags) above.
#### Specify STUN/TURN Server
On Unix based systems
```bash
@@ -180,8 +254,12 @@ On Windows
```bash
$env:RTC_CONFIG="rtc_config.json"; npm start
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` \
> to a JSON file including the configuration. \
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: \
> https://gabrieltanner.org/blog/turn-server/
>
> Default configuration:
> ```json
@@ -190,19 +268,44 @@ $env:RTC_CONFIG="rtc_config.json"; npm start
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
#### Debug Mode
On Unix based systems
```bash
DEBUG_MODE="true" npm start
```
On Windows
```bash
$env:DEBUG_MODE="true"; npm start
```
> Use this flag to enable debugging info about the connecting peers IP addresses. \
> This is quite useful to check whether the [#HTTP-Server](#http-server) \
> is configured correctly, so the auto discovery feature works correctly. \
> Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the \
> PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is set up correctly. \
>Find your devices public IP by visiting https://www.whatismyip.com/.
>
> Preserve your clients' privacy. **Never use this flag in production!**
### Options / Flags
#### Local Run
```bash
@@ -210,9 +313,11 @@ npm start -- --localhost-only
```
> Only allow connections from localhost.
>
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
> Use this when deploying PairDrop with node to prevent \
> bypassing the proxy by reaching the Docker container directly.
#### Automatic restart on error
```bash
@@ -234,13 +339,19 @@ npm start -- --rate-limit
```bash
npm start -- --include-ws-fallback
```
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
> Provides PairDrop to clients with an included websocket fallback \
> if the peer to peer WebRTC connection is not available to the client.
>
> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance.
> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
> This is not used on the official https://pairdrop.net, \
but you can activate it on your self-hosted instance. \
> This is especially useful if you connect to your instance \
> via a VPN as most VPN services block WebRTC completely in order to hide your real IP address.
> ([Read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
>
> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer!
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
> **Warning:** All traffic sent between devices using this fallback \
> is routed through the server and therefor not peer to peer! \
> Beware that the traffic routed via this fallback is readable by the server. \
> Only ever use this on instances you can trust. \
> Additionally, beware that all traffic using this fallback debits the servers data plan.
<br>
@@ -254,10 +365,14 @@ npm run start:prod
```bash
npm run start:prod -- --localhost-only --include-ws-fallback
```
> To prevent connections to the node server from bypassing the proxy server you should always use "--localhost-only" on production.
> To prevent connections to the node server from bypassing \
> the proxy server you should always use "--localhost-only" on production.
## HTTP-Server
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible.
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \
Otherwise, all clients will be mutually visible.
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
### Using nginx
#### Allow http and https requests
@@ -336,10 +451,10 @@ a2enmod proxy_wstunnel
<br>
Create a new configuration file under `/etc/apache2/sites-available` (on debian)
Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
**pairdrop.conf**
#### Allow http and https requests
#### Allow HTTP and HTTPS requests
```apacheconf
<VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/
@@ -356,7 +471,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
</VirtualHost>
```
#### Automatic http to https redirect:
#### Automatic HTTP to HTTPS redirect:
```apacheconf
<VirtualHost *:80>
Redirect permanent / https://127.0.0.1:3000/
@@ -369,7 +484,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
</VirtualHost>
```
Activate the new virtual host and reload apache:
Activate the new virtual host and reload Apache:
```bash
a2ensite pairdrop
```
@@ -393,28 +508,38 @@ Then, clone the repository and run docker-compose:
docker-compose up -d
```
Now point your browser to `http://localhost:8080`.
Now point your web browser to `http://localhost:8080`.
- To restart the containers run `docker-compose restart`.
- To stop the containers run `docker-compose stop`.
- To debug the NodeJS server run `docker logs pairdrop_node_1`.
- To restart the containers, run `docker-compose restart`.
- To stop the containers, run `docker-compose stop`.
- To debug the NodeJS server, run `docker logs pairdrop_node_1`.
<br>
## Testing PWA related features
PWAs require that the app is served under a correctly set up and trusted TLS endpoint.
PWAs requires the app to be served under a correctly set up and trusted TLS endpoint.
The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation.
The NGINX container creates a CA certificate and a website certificate for you. \
To correctly set the common name of the certificate, \
you need to change the FQDN environment variable in `docker/fqdn.env` \
to the fully qualified domain name of your workstation.
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \
For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \
Install that certificate to the trust store of your operating system. \
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. \
- On macOS, double-click the installed CA certificate in `Keychain Access`, \
- expand `Trust`, and select `Always Trust` for SSL. \
- Firefox uses its own trust store. To install the CA, \
- point Firefox at `http://<Your FQDN>:8080/ca.crt`. \
- When prompted, select `Trust this CA to identify websites` and click *OK*. \
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \
- Additionally, after installing a new cert, \
- you need to clear the Storage (DevTools → Application → Clear storage → Clear site data).
Please note that the certificates (CA and webserver cert) expire after a day.
Also, whenever you restart the nginx docker, container new certificates are created.
Also, whenever you restart the NGINX Docker, container new certificates are created.
The site is served on `https://<Your FQDN>:8443`.

View File

@@ -5,7 +5,7 @@ The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progres
This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows.
1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api)
2. Install PairDrop by visiting https://pairdrop.net/ with the Edge browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-).
2. Install PairDrop by visiting https://pairdrop.net/ with the Edge web browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-).
3. You are done! You can now send most files one at a time via PairDrop:
_context menu > Open with > PairDrop_
@@ -13,7 +13,8 @@ This is still experimental and must be enabled via a flag **before** the PWA is
[//]: # (Todo: add screenshots)
### Sending multiple files to PairDrop
Outstandingly, it is also possible to send multiple files to PairDrop via the context menu by adding PairDrop to the `Send to` menu:
Outstandingly, it is also possible to send multiple files to PairDrop \
via the context menu by adding PairDrop to the `Send to` menu:
1. [Register PairDrop as file handler](#registering-to-open-files-with-pairdrop)
2. Hit Windows Key+R, type: `shell:programs` and hit Enter.
3. Copy the PairDrop shortcut from the directory
@@ -26,17 +27,18 @@ Outstandingly, it is also possible to send multiple files to PairDrop via the co
[//]: # (Todo: add screenshots)
## Send directly from share menu on iOS
I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu
I created an iOS shortcut to send images, files, folder, URLs \
or text directly from the share-menu
https://routinehub.co/shortcut/13990/
[//]: # (Todo: add doku with screenshots)
## Send directly from share menu on Android
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented but not yet tested.
When the PWA is installed, it should register itself to the share-menu of the device automatically.
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented.
When the PWA is installed, it will register itself to the share-menu of the device automatically.
This feature is still under development. Please test this feature and create an issue if it does not work.
## Send directly via command-line interface
Send files or text with PairDrop via command-line interface.
@@ -63,7 +65,7 @@ On Windows Command Prompt you need to use bash: `bash pairdrop -h`
Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
#### Linux
1. Put file in a preferred folder e.g. `/usr/local/bin`
1. Put the file in a preferred folder e.g. `/usr/local/bin`
2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop`
3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing
`export PATH=$PATH:/opt/pairdrop-cli`
@@ -74,7 +76,7 @@ Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
#### Windows
1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli`
2. Search for and open `Edit environment variables for your account`
3. Click `Environment Variables...`
3. Click `Environment Variables`
4. Under *System Variables* select `Path` and click *Edit...*
5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed
6. Reopen Command prompt window

View File

@@ -3,48 +3,80 @@
Encryption is mandatory for WebRTC connections and completely done by the browser itself.
When the peers are first connecting, a channel is created by exchanging their signaling information.
This signaling information includes some sort of public key and is specific to the clients ip address.
That is what the STUN Server is used for: it simply returns your public IP address as you only know your local ip address
When the peers are first connecting, \
a channel is created by exchanging their signaling info. \
This signaling information includes some sort of public key \
and is specific to the clients IP address. \
That is what the STUN Server is used for: \
it simply returns your public IP address \
as you only know your local ip address \
if behind a NAT (router).
The transfer of the signaling information is done by the PairDrop / Snapdrop server using secure websockets.
After that the channel itself is completely peer-2-peer and all information can only be decrypted by the receiver.
When the two peers are on the same network or when they are not behind any NAT system (which they are always for classic
Snapdrop and for not paired users on PairDrop) the files are send directly peer to peer.
The transfer of the signaling info is done by the \
PairDrop / Snapdrop server using secure websockets. \
After that the channel itself is completely peer-to-peer \
and all info can only be decrypted by the receiver. \
When the two peers are on the same network \
or when they are not behind any NAT system \
(which they are always for classic \
Snapdrop and for not paired users on PairDrop) \
the files are send directly peer-to-peer.
When a user is behind a NAT (behind a router) the contents are channeled through a TURN server.
But again, the contents send via the channel can only be decrypted by the receiver. So a rogue TURN server could only
see that there is a connection, but not what is sent. Obviously, connections which are channeled through a TURN server
are not as fast as peer to peer.
When a user is behind a NAT (behind a router) \
the contents are channeled through a TURN server. \
But again, the contents send via the channel \
can only be decrypted by the receiver. \
So a rogue TURN server could only \
see that there is a connection, but not what is sent. \
Obviously, connections which are channeled through a TURN server \
are not as fast as peer-to-peer.
The selection whether a TURN server is needed or not is also done automatically by the browser.
It simply iterated through the configured RTC iceServers and checks what works. Only if the STUN server is not sufficient,
The selection whether a TURN server is needed \
or not is also done automatically by the web browser. \
It simply iterated through the configured \
RTC iceServers and checks what works. \
Only if the STUN server is not sufficient, \
the TURN server is used.
![img](https://www.wowza.com/wp-content/uploads/WeRTC-Encryption-Diagrams-01.jpg)
_Diagram created by wowza.com_
Good thing: if your device has an IPv6 address it is uniquely reachable by that address. As I understand it, when both devices are using IPv6 addresses there is no need for a TURN server in any scenario.
Good thing: if your device has an IPv6 address \
it is uniquely reachable by that address. \
As I understand it, when both devices are using \
IPv6 addresses there is no need for a TURN server in any scenario.
To learn more take a look at https://www.wowza.com/blog/webrtc-encryption-and-security which gives a good insight into stun, turn and webrtc
Learn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \
which gives a good insight into STUN, TURN and WebRTC.
## Device Pairing
The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
It works by creating long secrets that are served by the server to the initiating and requesting pair peer,
when the inserted key is correct. These long secrets are then saved to an indexedDB database in the browser.
IndexedDB is somewhat the successor of localStorage as saved data is shared between all tabs.
It goes one step further by making the data persistent and available offline if implemented to a PWA.
It works by creating long secrets that are served \
by the server to the initiating and requesting pair peer, \
when the inserted key is correct. \
These long secrets are then saved to an \
indexedDB database in the web browser. \
IndexedDB is somewhat the successor of localStorage \
as saved data is shared between all tabs. \
It goes one step further by making the data persistent \
and available offline if implemented to a PWA.
All secrets a client has saved to its database are send to the PairDrop server. Peers with a common secret are discoverable
to each other analog to peers with the same ip-address are discoverable to each other.
All secrets a client has saved to its database \
are sent to the PairDrop server. \
Peers with a common secret are discoverable \
to each other analog to peers with the same \
IP address are discoverable by each other.
What I really like about this approach, and the reason why I implemented it, is that devices on the same network are always
visible regardless whether any devices are paired or not. The main user flow is never obstructed. Paired devices are simply
shown additionally. This makes it in my idea better than the idea of using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).
What I really like about this approach (and the reason I implemented it) \
is that devices on the same network are always \
visible regardless whether any devices are paired or not. \
The main user flow is never obstructed. \
Paired devices are simply shown additionally. \
This makes it in my idea better than the idea of \
using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).
[< Back](/README.md)

539
index.js
View File

@@ -63,14 +63,6 @@ const rtcConfig = process.env.RTC_CONFIG
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "stun:openrelay.metered.ca:80"
},
{
"urls": "turn:openrelay.metered.ca:443",
"username": "openrelayproject",
"credential": "openrelayproject"
}
]
};
@@ -98,6 +90,23 @@ if (process.argv.includes('--include-ws-fallback')) {
app.use(express.static('public'));
}
const debugMode = process.env.DEBUG_MODE === "true";
if (debugMode) {
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
}
let ipv6_lcl;
if (process.env.IPV6_LOCALIZE) {
ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE);
if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) {
console.error("IPV6_LOCALIZE must be an integer between 1 and 7");
return;
}
console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments");
}
app.use(function(req, res) {
res.redirect('/');
});
@@ -115,14 +124,24 @@ if (process.argv.includes('--localhost-only')) {
server.listen(port);
}
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(err);
console.info("Error EADDRINUSE received, exiting process without restarting process...");
process.exit(0)
}
});
class PairDropServer {
constructor() {
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
this._rooms = {};
this._roomSecrets = {};
this._rooms = {}; // { roomId: peers[] }
this._roomSecrets = {}; // { pairKey: roomSecret }
this._keepAliveTimers = {};
console.log('PairDrop is running on port', port);
}
@@ -130,12 +149,13 @@ class PairDropServer {
_onConnection(peer) {
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
this._keepAlive(peer);
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
});
this._joinRoom(peer);
// send displayName
this._send(peer, {
@@ -144,7 +164,7 @@ class PairDropServer {
displayName: peer.name.displayName,
deviceName: peer.name.deviceName,
peerId: peer.id,
peerIdHash: peer.id.hashCode128BitSalted()
peerIdHash: hasher.hashCodeSalted(peer.id)
}
});
}
@@ -162,16 +182,16 @@ class PairDropServer {
this._onDisconnect(sender);
break;
case 'pong':
sender.lastBeat = Date.now();
this._setKeepAliveTimerToNow(sender);
break;
case 'join-ip-room':
this._joinIpRoom(sender);
break;
case 'room-secrets':
this._onRoomSecrets(sender, message);
break;
case 'room-secret-deleted':
this._onRoomSecretDeleted(sender, message);
break;
case 'room-secrets-cleared':
this._onRoomSecretsCleared(sender, message);
case 'room-secrets-deleted':
this._onRoomSecretsDeleted(sender, message);
break;
case 'pair-device-initiate':
this._onPairDeviceInitiate(sender);
@@ -182,8 +202,17 @@ class PairDropServer {
case 'pair-device-cancel':
this._onPairDeviceCancel(sender);
break;
case 'resend-peers':
this._notifyPeers(sender);
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
break;
case 'create-public-room':
this._onCreatePublicRoom(sender);
break;
case 'join-public-room':
this._onJoinPublicRoom(sender, message);
break;
case 'leave-public-room':
this._onLeavePublicRoom(sender);
break;
case 'signal':
default:
@@ -192,7 +221,9 @@ class PairDropServer {
}
_signalAndRelay(sender, message) {
const room = message.roomType === 'ip' ? sender.ip : message.roomSecret;
const room = message.roomType === 'ip'
? sender.ip
: message.roomId;
// relay message to recipient
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
@@ -208,89 +239,88 @@ class PairDropServer {
}
_onDisconnect(sender) {
this._leaveRoom(sender, 'ip', '', true);
this._disconnect(sender);
}
_disconnect(sender) {
this._removePairKey(sender.pairKey);
sender.pairKey = null;
this._cancelKeepAlive(sender);
delete this._keepAliveTimers[sender.id];
this._leaveIpRoom(sender, true);
this._leaveAllSecretRooms(sender, true);
this._removeRoomKey(sender.roomKey);
sender.roomKey = null;
this._leavePublicRoom(sender, true);
sender.socket.terminate();
}
_onRoomSecrets(sender, message) {
if (!message.roomSecrets) return;
const roomSecrets = message.roomSecrets.filter(roomSecret => {
return /^[\x00-\x7F]{64}$/.test(roomSecret);
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
})
if (!roomSecrets) return;
this._joinSecretRooms(sender, roomSecrets);
}
_onRoomSecretDeleted(sender, message) {
this._deleteSecretRoom(sender, message.roomSecret)
}
_onRoomSecretsCleared(sender, message) {
_onRoomSecretsDeleted(sender, message) {
for (let i = 0; i<message.roomSecrets.length; i++) {
this._deleteSecretRoom(sender, message.roomSecrets[i]);
this._deleteSecretRoom(message.roomSecrets[i]);
}
}
_deleteSecretRoom(sender, roomSecret) {
_deleteSecretRoom(roomSecret) {
const room = this._rooms[roomSecret];
if (room) {
for (const peerId in room) {
const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
}
}
this._notifyPeers(sender);
}
if (!room) return;
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
for (const peerId in room) {
const peer = room[peerId];
this._leaveSecretRoom(peer, roomSecret, true);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
_onPairDeviceInitiate(sender) {
let roomSecret = this.getRandomString(64);
let roomKey = this._createRoomKey(sender, roomSecret);
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
sender.roomKey = roomKey;
let roomSecret = randomizer.getRandomString(256);
let pairKey = this._createPairKey(sender, roomSecret);
if (sender.pairKey) {
this._removePairKey(sender.pairKey);
}
sender.pairKey = pairKey;
this._send(sender, {
type: 'pair-device-initiated',
roomSecret: roomSecret,
roomKey: roomKey
pairKey: pairKey
});
this._joinRoom(sender, 'secret', roomSecret);
this._joinSecretRoom(sender, roomSecret);
}
_onPairDeviceJoin(sender, message) {
if (sender.roomKeyRate >= 10) {
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
if (sender.rateLimitReached()) {
this._send(sender, { type: 'join-key-rate-limit' });
return;
}
sender.roomKeyRate += 1;
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
this._send(sender, { type: 'pair-device-join-key-invalid' });
return;
}
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
const creator = this._roomSecrets[message.roomKey].creator;
this._removeRoomKey(message.roomKey);
const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
const creator = this._roomSecrets[message.pairKey].creator;
this._removePairKey(message.pairKey);
this._send(sender, {
type: 'pair-device-joined',
roomSecret: roomSecret,
@@ -301,139 +331,222 @@ class PairDropServer {
roomSecret: roomSecret,
peerId: sender.id
});
this._joinRoom(sender, 'secret', roomSecret);
this._removeRoomKey(sender.roomKey);
this._joinSecretRoom(sender, roomSecret);
this._removePairKey(sender.pairKey);
}
_onPairDeviceCancel(sender) {
if (sender.roomKey) {
this._send(sender, {
type: 'pair-device-canceled',
roomKey: sender.roomKey,
});
this._removeRoomKey(sender.roomKey);
}
const pairKey = sender.pairKey
if (!pairKey) return;
this._removePairKey(pairKey);
this._send(sender, {
type: 'pair-device-canceled',
pairKey: pairKey,
});
}
_createRoomKey(creator, roomSecret) {
let roomKey;
_onCreatePublicRoom(sender) {
let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
this._send(sender, {
type: 'public-room-created',
roomId: publicRoomId
});
this._joinPublicRoom(sender, publicRoomId);
}
_onJoinPublicRoom(sender, message) {
if (sender.rateLimitReached()) {
this._send(sender, { type: 'join-key-rate-limit' });
return;
}
if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
return;
}
this._leavePublicRoom(sender);
this._joinPublicRoom(sender, message.publicRoomId);
}
_onLeavePublicRoom(sender) {
this._leavePublicRoom(sender, true);
this._send(sender, { type: 'public-room-left' });
}
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
// notify all other peers
for (const peerId in this._rooms[oldRoomSecret]) {
const peer = this._rooms[oldRoomSecret][peerId];
this._send(peer, {
type: 'room-secret-regenerated',
oldRoomSecret: oldRoomSecret,
newRoomSecret: newRoomSecret,
});
peer.removeRoomSecret(oldRoomSecret);
}
delete this._rooms[oldRoomSecret];
}
_createPairKey(creator, roomSecret) {
let pairKey;
do {
// get randomInt until keyRoom not occupied
roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
} while (roomKey in this._roomSecrets)
pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
} while (pairKey in this._roomSecrets)
this._roomSecrets[roomKey] = {
this._roomSecrets[pairKey] = {
roomSecret: roomSecret,
creator: creator
}
return roomKey;
return pairKey;
}
_removeRoomKey(roomKey) {
_removePairKey(roomKey) {
if (roomKey in this._roomSecrets) {
this._roomSecrets[roomKey].creator.roomKey = null
delete this._roomSecrets[roomKey];
}
}
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret;
_joinIpRoom(peer) {
this._joinRoom(peer, 'ip', peer.ip);
}
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
_joinSecretRoom(peer, roomSecret) {
this._joinRoom(peer, 'secret', roomSecret);
// add secret to peer
peer.addRoomSecret(roomSecret);
}
_joinPublicRoom(peer, publicRoomId) {
// prevent joining of 2 public rooms simultaneously
this._leavePublicRoom(peer);
this._joinRoom(peer, 'public-id', publicRoomId);
peer.publicRoomId = publicRoomId;
}
_joinRoom(peer, roomType, roomId) {
// roomType: 'ip', 'secret' or 'public-id'
if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
this._leaveRoom(peer, roomType, roomId);
}
// if room doesn't exist, create it
if (!this._rooms[room]) {
this._rooms[room] = {};
if (!this._rooms[roomId]) {
this._rooms[roomId] = {};
}
this._notifyPeers(peer, roomType, roomSecret);
this._notifyPeers(peer, roomType, roomId);
// add peer to room
this._rooms[room][peer.id] = peer;
// add secret to peer
if (roomType === 'secret') {
peer.addRoomSecret(roomSecret);
}
this._rooms[roomId][peer.id] = peer;
}
_leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (!this._rooms[room] || !this._rooms[room][peer.id]) return;
this._cancelKeepAlive(this._rooms[room][peer.id]);
_leaveIpRoom(peer, disconnect = false) {
this._leaveRoom(peer, 'ip', peer.ip, disconnect);
}
// delete the peer
delete this._rooms[room][peer.id];
_leaveSecretRoom(peer, roomSecret, disconnect = false) {
this._leaveRoom(peer, 'secret', roomSecret, disconnect)
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];
} else {
// notify all other peers
for (const otherPeerId in this._rooms[room]) {
const otherPeer = this._rooms[room][otherPeerId];
this._send(otherPeer, {
type: 'peer-left',
peerId: peer.id,
roomType: roomType,
roomSecret: roomSecret,
disconnect: disconnect
});
}
}
//remove secret from peer
if (roomType === 'secret') {
peer.removeRoomSecret(roomSecret);
peer.removeRoomSecret(roomSecret);
}
_leavePublicRoom(peer, disconnect = false) {
if (!peer.publicRoomId) return;
this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
peer.publicRoomId = null;
}
_leaveRoom(peer, roomType, roomId, disconnect = false) {
if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
// remove peer from room
delete this._rooms[roomId][peer.id];
// delete room if empty and abort
if (!Object.keys(this._rooms[roomId]).length) {
delete this._rooms[roomId];
return;
}
// notify all other peers that remain in room that peer left
for (const otherPeerId in this._rooms[roomId]) {
const otherPeer = this._rooms[roomId][otherPeerId];
let msg = {
type: 'peer-left',
peerId: peer.id,
roomType: roomType,
roomId: roomId,
disconnect: disconnect
};
this._send(otherPeer, msg);
}
}
_notifyPeers(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (!this._rooms[room]) return;
_notifyPeers(peer, roomType, roomId) {
if (!this._rooms[roomId]) return;
// notify all other peers
for (const otherPeerId in this._rooms[room]) {
// notify all other peers that peer joined
for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
const otherPeer = this._rooms[room][otherPeerId];
this._send(otherPeer, {
const otherPeer = this._rooms[roomId][otherPeerId];
let msg = {
type: 'peer-joined',
peer: peer.getInfo(),
roomType: roomType,
roomSecret: roomSecret
});
roomId: roomId
};
this._send(otherPeer, msg);
}
// notify peer about the other peers
// notify peer about peers already in the room
const otherPeers = [];
for (const otherPeerId in this._rooms[room]) {
for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
otherPeers.push(this._rooms[room][otherPeerId].getInfo());
otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
}
this._send(peer, {
let msg = {
type: 'peers',
peers: otherPeers,
roomType: roomType,
roomSecret: roomSecret
});
roomId: roomId
};
this._send(peer, msg);
}
_joinSecretRooms(peer, roomSecrets) {
for (let i=0; i<roomSecrets.length; i++) {
this._joinRoom(peer, 'secret', roomSecrets[i])
this._joinSecretRoom(peer, roomSecrets[i])
}
}
_leaveAllSecretRooms(peer, disconnect = false) {
for (let i=0; i<peer.roomSecrets.length; i++) {
this._leaveRoom(peer, 'secret', peer.roomSecrets[i], disconnect);
this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);
}
}
@@ -446,24 +559,35 @@ class PairDropServer {
_keepAlive(peer) {
this._cancelKeepAlive(peer);
let timeout = 500;
if (!peer.lastBeat) {
peer.lastBeat = Date.now();
let timeout = 1000;
if (!this._keepAliveTimers[peer.id]) {
this._keepAliveTimers[peer.id] = {
timer: 0,
lastBeat: Date.now()
};
}
if (Date.now() - peer.lastBeat > 2 * timeout) {
this._leaveRoom(peer);
this._leaveAllSecretRooms(peer);
if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {
// Disconnect peer if unresponsive for 10s
this._disconnect(peer);
return;
}
this._send(peer, { type: 'ping' });
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);
}
_cancelKeepAlive(peer) {
if (peer && peer.timerId) {
clearTimeout(peer.timerId);
if (this._keepAliveTimers[peer.id]?.timer) {
clearTimeout(this._keepAliveTimers[peer.id].timer);
}
}
_setKeepAliveTimerToNow(peer) {
if (this._keepAliveTimers[peer.id]?.lastBeat) {
this._keepAliveTimers[peer.id].lastBeat = Date.now();
}
}
}
@@ -480,7 +604,7 @@ class Peer {
this._setIP(request);
// set peer id
this._setPeerId(request)
this._setPeerId(request);
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
@@ -488,13 +612,22 @@ class Peer {
// set name
this._setName(request);
// for keepalive
this.timerId = 0;
this.lastBeat = Date.now();
this.requestRate = 0;
this.roomSecrets = [];
this.roomKey = null;
this.roomKeyRate = 0;
this.publicRoomId = null;
}
rateLimitReached() {
// rate limit implementation: max 10 attempts every 10s
if (this.requestRate >= 10) {
return true;
}
this.requestRate += 1;
setTimeout(_ => this.requestRate -= 1, 10000);
return false;
}
_setIP(request) {
@@ -510,6 +643,25 @@ class Peer {
if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7);
let ipv6_was_localized = false;
if (ipv6_lcl && this.ip.includes(':')) {
this.ip = this.ip.split(':',ipv6_lcl).join(':');
ipv6_was_localized = true;
}
if (debugMode) {
console.debug("----DEBUGGING-PEER-IP-START----");
console.debug("remoteAddress:", request.connection.remoteAddress);
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
if (ipv6_was_localized)
console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment");
console.debug("PairDrop uses:", this.ip);
console.debug("IP is private:", this.ipIsPrivate(this.ip));
console.debug("if IP is private, '127.0.0.1' is used instead");
console.debug("----DEBUGGING-PEER-IP-END----");
}
// IPv4 and IPv6 use different values to refer to localhost
// put all peers on the same network as the server into the same room as well
if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {
@@ -591,7 +743,7 @@ class Peer {
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: this.id.hashCode()
seed: cyrb53(this.id)
})
this.name = {
@@ -617,7 +769,7 @@ class Peer {
}
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === peerId.hashCode128BitSalted();
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) {
@@ -633,39 +785,50 @@ class Peer {
}
}
Object.defineProperty(String.prototype, 'hashCode', {
value: function() {
return cyrb53(this);
}
});
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
value: function() {
return hasher.hashCode128BitSalted(this);
}
});
const hasher = (() => {
let seeds;
let password;
return {
hashCode128BitSalted(str) {
if (!seeds) {
// seeds are created on first call to salt hash.
seeds = [4];
for (let i=0; i<4; i++) {
const randomBuffer = new Uint32Array(1);
crypto.webcrypto.getRandomValues(randomBuffer);
seeds[i] = randomBuffer[0];
}
hashCodeSalted(salt) {
if (!password) {
// password is created on first call.
password = randomizer.getRandomString(128);
}
let hashCode = "";
for (let i=0; i<4; i++) {
hashCode += cyrb53(str, seeds[i]);
}
return hashCode;
return crypto.createHash("sha3-512")
.update(password)
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
.digest("hex");
}
}
})()
const randomizer = (() => {
let charCodeLettersOnly = r => 65 <= r && r <= 90;
let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122;
return {
getRandomString(length, lettersOnly = false) {
const charCodeCondition = lettersOnly
? charCodeLettersOnly
: charCodeAllPrintableChars;
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
return charCodeCondition(r);
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
}
})()
/*

52
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "pairdrop",
"version": "1.5.0",
"version": "1.9.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.5.0",
"version": "1.9.4",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.34",
"express-rate-limit": "^7.1.1",
"ua-parser-js": "^1.0.36",
"unique-names-generator": "^4.3.0",
"ws": "^8.13.0"
"ws": "^8.14.2"
},
"engines": {
"node": ">=15"
@@ -204,11 +204,11 @@
}
},
"node_modules/express-rate-limit": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
"integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.1.tgz",
"integrity": "sha512-o5ye/a4EHCPQPju25Y4HChHybrCM9v37QtQDqXUDZGuD+HB7Cbu8ZhJP6/9RORcSNtkCpnEssa6oUgJgzc7ckQ==",
"engines": {
"node": ">= 12.9.0"
"node": ">= 16"
},
"peerDependencies": {
"express": "^4 || ^5"
@@ -583,9 +583,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==",
"funding": [
{
"type": "opencollective",
@@ -594,6 +594,10 @@
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
@@ -633,9 +637,9 @@
}
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"engines": {
"node": ">=10.0.0"
},
@@ -801,9 +805,9 @@
}
},
"express-rate-limit": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
"integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.1.tgz",
"integrity": "sha512-o5ye/a4EHCPQPju25Y4HChHybrCM9v37QtQDqXUDZGuD+HB7Cbu8ZhJP6/9RORcSNtkCpnEssa6oUgJgzc7ckQ==",
"requires": {}
},
"finalhandler": {
@@ -1070,9 +1074,9 @@
}
},
"ua-parser-js": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw=="
},
"unique-names-generator": {
"version": "4.7.1",
@@ -1095,9 +1099,9 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"requires": {}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.5.0",
"version": "1.9.4",
"description": "",
"main": "index.js",
"scripts": {
@@ -11,10 +11,10 @@
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.34",
"express-rate-limit": "^7.1.1",
"ua-parser-js": "^1.0.36",
"unique-names-generator": "^4.3.0",
"ws": "^8.13.0"
"ws": "^8.14.2"
},
"engines": {
"node": ">=15"

View File

@@ -38,7 +38,10 @@ openPairDrop()
else
xdg-open "$url"
fi
exit
}
setOs()
@@ -98,13 +101,19 @@ sendFiles()
[[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit
if [[ -d $path ]]; then
zipPathTemp="temp_${zipPath}"
zipPathTemp="${path}_pairdrop_temp.zip"
[[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit
echo "Processing directory..."
# Create zip files temporarily to send directory
zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
if [[ $OS == "Windows" ]];then
powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath}"
echo "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}"
powershell.exe -Command "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}"
else
zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
fi
if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPathTemp")
@@ -118,8 +127,12 @@ sendFiles()
echo "Processing file..."
# Create zip file temporarily to send file
zip -q -b /tmp/ "$zipPath" "$path"
if [[ $OS == "Windows" ]];then
powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath} -CompressionLevel Optimal"
else
zip -q -b /tmp/ "$zipPath" "$path"
fi
if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPath")
else
@@ -142,6 +155,7 @@ sendFiles()
hash=
fi
openPairDrop
exit
}

View File

@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no">
@@ -33,111 +33,296 @@
<meta name="twitter:image" content="images/logo_transparent_512x512.png">
<meta property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources -->
<link rel="preload" href="lang/en.json" as="fetch">
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="manifest" href="manifest.json">
</head>
<body translate="no">
<header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop">
<header class="row-reverse opacity-0">
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label">
<svg class="icon">
<use xlink:href="#info-outline" />
<use xlink:href="#info-outline"></use>
</svg>
</a>
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme" />
<use xlink:href="#icon-language-selector"></use>
</svg>
</a>
<a id="notification" class="icon-button" title="Enable Notifications" hidden>
</div>
<div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-auto"></use>
</svg>
</div>
<div>
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-light"></use>
</svg>
</div>
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-dark"></use>
</svg>
</div>
</div>
</div>
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#notifications" />
<use xlink:href="#notifications"></use>
</svg>
</a>
<a id="install" class="icon-button" title="Install PairDrop" hidden>
</div>
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#homescreen" />
<use xlink:href="#homescreen"></use>
</svg>
</a>
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
</div>
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#pair-device-icon" />
<use xlink:href="#pair-device-icon"></use>
</svg>
</a>
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
</div>
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon"></use>
</svg>
</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</div>
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#public-room-icon"></use>
</svg>
</div>
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden></div>
</header>
<!-- Center -->
<div id="center">
<div id="center" class="opacity-0">
<!-- Peers -->
<div class="x-peers-filler"></div>
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
<x-no-peers class="no-animation-on-load" data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg">
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text"></h2>
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text"></div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg">
<p id="paste-filename"></p>
</x-instructions>
</div>
<!-- Footer -->
<footer class="column">
<footer class="column opacity-0">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
<use xlink:href="#wifi-tethering"></use>
</svg>
<div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
<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>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon"></use>
</svg>
</div>
<div class="discovery-wrapper row">
<div class="row center">
<span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span>
</div>
<div class="row center">
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
</div>
</div>
</div>
</footer>
<!-- Language Select Dialog -->
<x-dialog id="language-select-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2>
</div>
<div class="language-buttons">
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></button>
<button class="button fw" value="ar">
<span>العربية</span>
<span>-</span>
<span>(Arabic)</span>
</button>
<button class="button fw" value="de">
<span>Deutsch</span>
<span>-</span>
<span>(German)</span>
</button>
<button class="button fw" value="en">
<span>English</span>
</button>
<button class="button fw" value="fr">
<span>Français</span>
<span>-</span>
<span>(French)</span>
</button>
<button class="button fw" value="id">
<span>Bahasa Indonesia</span>
<span>-</span>
<span>(Indonesian)</span>
</button>
<button class="button fw" value="it">
<span>Italiano</span>
<span>-</span>
<span>(Italian)</span>
</button>
<button class="button fw" value="nl">
<span>Nederlands</span>
<span>-</span>
<span>(Dutch)</span>
</button>
<button class="button fw" value="nb">
<span>Norsk</span>
<span>-</span>
<span>(Norwegian)</span>
</button>
<button class="button fw" value="ro">
<span>Română</span>
<span>-</span>
<span>(Romanian)</span>
</button>
<button class="button fw" value="ru">
<span>Русский язык</span>
<span>-</span>
<span>(Russian)</span>
</button>
<button class="button fw" value="es">
<span>Español</span>
<span>-</span>
<span>(Spanish)</span>
</button>
<button class="button fw" value="zh-CN">
<span>中文</span>
<span>-</span>
<span>(Chinese)</span>
</button>
<button class="button fw" value="ja">
<span>日本語</span>
<span>-</span>
<span>(Japanese)</span>
</button>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Pair Device Dialog -->
<x-dialog id="pair-device-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Pair Devices</h2>
<div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<div id="key-input-container">
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" type="button" close>Cancel</button>
<div class="row center">
<div class="column">
<div class="center key-qr-code" data-i18n-key="dialogs.pair-devices-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr">000 000</h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column">
<div class="input-key-container six-chars" dir="ltr">
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty"></div>
<div class="font-subheading center">
<p>
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text"></span>
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text"></u>
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text"></span>
</p>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Public Room Dialog -->
<x-dialog id="public-room-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<div class="row center">
<div class="column">
<h2 class="center" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row center">
<div class="column">
<div class="center key-qr-code" data-i18n-key="dialogs.public-room-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr"></h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column">
<div class="input-key-container" dir="ltr">
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="center row-reverse button-row">
<button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -147,24 +332,30 @@
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>would like to share</span>
<div class="row center">
<div class="column">
<h2 class="center"></h2>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row center">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
<div class="row-reverse center button-row">
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus></button>
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -173,24 +364,31 @@
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>has sent</span>
<div class="row center">
<div class="column">
<h2 class="center"></h2>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row center">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
<div class="row-reverse center button-row">
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus></button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@@ -200,16 +398,27 @@
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span class="display-name"></span>
<div class="row center">
<div class="column">
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
<div class="row center display-name-wrapper">
<div class="column">
<div class="text-center">
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text"></span>
<span class="display-name badge"></span>
</div>
</div>
</div>
<div class="row">
<div class="column fw">
<div id="text-input" class="textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@@ -219,16 +428,23 @@
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
<div class="row center">
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button>
<div class="row center">
<div class="text-center">
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column fw">
<div id="text" class="textarea fw"></div>
</div>
</div>
<div class="row-reverse center button-row">
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text"></button>
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -237,119 +453,143 @@
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button>
<div class="row-reverse center button-row">
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast id="toast" class="row" shadow="1"></x-toast>
<x-toast id="toast" class="row center" shadow="1"></x-toast>
</div>
<!-- About Page -->
<x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<header class="row-reverse">
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label">
<svg class="icon">
<use xlink:href="#close-icon" />
<use xlink:href="#close-icon"></use>
</svg>
</a>
</header>
<section class="center column fade-in">
<section class="center column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
<use xlink:href="#wifi-tethering"></use>
</svg>
<h1>PairDrop</h1>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.9.4</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#github" />
<use xlink:href="#github"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#monetarization" />
<use xlink:href="#monetarization"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&amp;" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#twitter" />
<use xlink:href="#twitter"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#help-outline" />
<use xlink:href="#help-outline"></use>
</svg>
</a>
</div>
</section>
<x-background></x-background>
</x-about>
<canvas class="circles opacity-0"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
<symbol id="wifi-tethering" viewBox="0 0 24 24">
<path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z"></path>
</symbol>
<symbol id=desktop-mac viewBox="0 0 24 24">
<symbol id="desktop-mac" viewBox="0 0 24 24">
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z"></path>
</symbol>
<symbol id=phone-iphone viewBox="0 0 24 24">
<symbol id="phone-iphone" viewBox="0 0 24 24">
<path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"></path>
</symbol>
<symbol id=tablet-mac viewBox="0 0 24 24">
<symbol id="tablet-mac" viewBox="0 0 24 24">
<path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z"></path>
</symbol>
<symbol id=info-outline viewBox="0 0 24 24">
<symbol id="info-outline" viewBox="0 0 24 24">
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
</symbol>
<symbol id=close-icon viewBox="0 0 24 24">
<symbol id="close-icon" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</symbol>
<symbol id=help-outline viewBox="0 0 24 24">
<symbol id="help-outline" viewBox="0 0 24 24">
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
</symbol>
<symbol id="twitter">
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z"></path>
</symbol>
<symbol id="github">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</symbol>
<g id="notifications">
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"></path>
</g>
<symbol id="homescreen">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z" />
<path fill="none" d="M0 0h24v24H0V0z" />
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path>
</symbol>
<symbol id="monetarization">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path>
</symbol>
<symbol id="icon-theme" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
<symbol id="icon-theme-auto" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"></path></svg>
</symbol>
<symbol id="icon-theme-light" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"></path></svg>
</symbol>
<symbol id="icon-theme-dark" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"></rect><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path>
</symbol>
<symbol id="pair-device-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"></path>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"></path>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"></path>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"></path>
</symbol>
<symbol id="public-room-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"></path>
</symbol>
<symbol id="icon-language-selector" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"></path>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/util.js"></script>
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script>
<script src="scripts/theme.js" async></script>
<script src="scripts/qrcode.js" async></script>
<script src="scripts/util.js"></script>
<script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script>
<!-- Sounds -->
@@ -365,3 +605,4 @@
</x-noscript>
</noscript>
</body>
</html>

159
public/lang/ar.json Normal file
View File

@@ -0,0 +1,159 @@
{
"footer": {
"webrtc": "إذا لم يكن WebRTC متاحًا.",
"public-room-devices_title": "يمكن اكتشافك بواسطة الأجهزة الموجودة في هذه الغرفة العامة المستقلة عن الشبكة.",
"display-name_data-placeholder": "تحميل …",
"display-name_title": "قم بتحرير اسم جهازك بشكل دائم",
"traffic": "حركة المرور هي",
"paired-devices_title": "يمكن اكتشافك بواسطة الأجهزة المقترنة في جميع الأوقات بشكل مستقل عن الشبكة.",
"public-room-devices": "في الغرفة {{roomId}}",
"paired-devices": "بواسطة الأجهزة المقترنة",
"on-this-network": "على هذه الشبكة",
"routed": "توجيهّا من خلال الخادم",
"discovery": "يمكنك اكتشاف:",
"on-this-network_title": "يمكن للجميع اكتشافك على هذه الشبكة.",
"known-as": "‌أنت معروف بأنك:"
},
"notifications": {
"request-title": "يرغب {{name}} في نقل {{count}} {{descriptor}}",
"unfinished-transfers-warning": "هناك تحويلات غير مكتملة. هل أنت متأكد أنك تريد إغلاق PairDrop؟",
"message-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"rate-limit-join-key": "تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.",
"connecting": "يتصل …",
"pairing-key-invalidated": "المفتاح {{key}} خاطئ.",
"pairing-key-invalid": "مُفتاح خاطئ",
"connected": "متصل.",
"pairing-not-persistent": "الأجهزة المقترنة ليست ثابتة.",
"text-content-incorrect": "محتوى النص غير صحيح.",
"message-transfer-completed": "اكتمل نقل الرسالة.",
"file-transfer-completed": "اكتمل نقل الملف.",
"file-content-incorrect": "محتوى الملف غير صحيح.",
"files-incorrect": "الملفات غير صحيحة.",
"selected-peer-left": "مُحَدد الاجهزة المقترنة.",
"link-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"online": "لقد عدت متصلاً بالإنترنت",
"public-room-left": "الخروج من الغرفة العامة {{publicRoomId}}",
"copied-text": "نُسِخَ النص إلى الحافظة",
"display-name-random-again": "يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى.",
"display-name-changed-permanently": "يتم تغيير اسم العرض بشكل دائم.",
"copied-to-clipboard-error": "النسخ غير ممكن. انسخ يدويًا.",
"pairing-success": "الأجهزة المقترنة.",
"clipboard-content-incorrect": "محتوى الحافظة غير صحيح.",
"display-name-changed-temporarily": "تم تغيير اسم العرض لهذه الجلسة فقط.",
"copied-to-clipboard": "تم النسخ إلى الحافظة",
"offline": "انت غير متصل",
"pairing-tabs-error": "من المستحيل إقران علامتي تبويب متصفح الويب.",
"public-room-id-invalid": "معرف الغرفة غير صالح",
"click-to-download": "إضغط للتحميل",
"pairing-cleared": "جميع الأجهزة غير مقترنة.",
"notifications-enabled": "تم تمكين الإشعارات.",
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة.",
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة.",
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
"download-successful": "تم تحميل {{descriptor}}",
"click-to-show": "اضغط للعرض"
},
"header": {
"cancel-paste-mode": "تمّ",
"theme-auto_title": "تكيٌف المظهر مع النظام",
"install_title": "تثبيت PairDrop",
"theme-dark_title": "إستخدام دائما المظهر المظلم",
"pair-device_title": "قم بإقران أجهزتك بشكل دائم",
"join-public-room_title": "انضم إلى الغرفة العامة مؤقتًا",
"notification_title": "تشغيل الإشعارات",
"edit-paired-devices_title": "تعديل الأجهزة المقترنة",
"language-selector_title": "إختر اللغةعربي",
"about_title": "حول PairDrop",
"about_aria-label": "افتح حول PairDrop",
"theme-light_title": "إستخدم دائماً المظهر الفاتح"
},
"instructions": {
"x-instructions_mobile": "انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة",
"click-to-send": "انقر للإرسال",
"activate-paste-mode-and-other-files": "و{{count}} ملفات أخرى",
"tap-to-send": "انقر للإرسال",
"activate-paste-mode-base": "افتح PairDrop على الأجهزة الأخرى للإرسال",
"no-peers-subtitle": "قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من إكتشافها على الشبكات الأخرى",
"activate-paste-mode-shared-text": "النص المشترك",
"x-instructions_desktop": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"no-peers-title": "افتح PairDrop على الأجهزة الأخرى لإرسال الملفات",
"x-instructions_data-drop-bg": "حرر لتحديد المستلم",
"no-peers_data-drop-bg": "حرر لتحديد المستلم",
"x-instructions_data-drop-peer": "قم بالتحرير لإرسالها إلى النظير"
},
"peer-ui": {
"processing": "مُعالجة …",
"click-to-send-paste-mode": "انقر للإرسال {{descriptor}}",
"click-to-send": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"waiting": "يُرجى الإنتظار…",
"connection-hash": "للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين",
"preparing": "يقترن…",
"transferring": "جارٍ النقل…"
},
"dialogs": {
"base64-paste-to-send": "الصق هنا لإرسال {{type}}",
"auto-accept-instructions-2": "لقبول جميع الملفات المرسلة من هذا الجهاز تلقائيًا.",
"receive-text-title": "تلقيت رسالة",
"edit-paired-devices-title": "تحرير الأجهزة المقترنة",
"cancel": "إلغاء",
"auto-accept-instructions-1": "تفعيل",
"pair-devices-title": "إقران الأجهزة بشكل دائم",
"download": "تحميل",
"title-file": "ملف",
"base64-processing": "مُعالجة…",
"decline": "رفض",
"receive-title": "تم الاستلام {{descriptor}}",
"leave": "مُغادرة",
"join": "انضمام",
"title-image-plural": "صور",
"send": "ارسال",
"base64-tap-to-paste": "انقر هنا للصق {{type}}",
"base64-text": "نص",
"copy": "نسخ",
"file-other-description-image": "وصورة واحدة أخرى",
"temporary-public-room-title": "غرفة عامة مؤقتة",
"base64-files": "ملفات",
"has-sent": "ارسلت:",
"file-other-description-file": "وملف واحد آخر",
"close": "إغلاق",
"system-language": "لغة النظام",
"unpair": "إلغاء الإقتران",
"title-image": "صورة",
"file-other-description-file-plural": "و{{count}} ملفات أخرى",
"would-like-to-share": "ترغب في المشاركة",
"send-message-to": "أرسال رسالة إلى",
"language-selector-title": "إختر اللُغة",
"pair": "إقتران",
"hr-or": "او",
"scan-qr-code": "أو مسح رمز الاستجابة السريعة.",
"input-key-on-this-device": "أدخل هذا المفتاح على جهاز آخر",
"download-again": "تحميل مرة أخرى",
"accept": "قبول",
"paired-devices-wrapper_data-empty": "لا توجد أجهزة مقترنة.",
"enter-key-from-another-device": "أدخل المفتاح من جهاز آخر هنا.",
"share": "مُشاركة",
"auto-accept": "قبول تلقائي",
"title-file-plural": "ملفات",
"send-message-title": "إرسال رسالة",
"input-room-id-on-another-device": "‌أدخل معرف الغرفة هذا على جهاز آخر ما ",
"file-other-description-image-plural": "و{{count}} صور أخرى",
"enter-room-id-from-another-device": "أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة."
},
"about": {
"claim": "أسهل طريقة لنقل الملفات عبر الأجهزة",
"tweet_title": "غرّد حول PairDrop",
"close-about_aria-label": "إغلاق حول PairDrop",
"buy-me-a-coffee_title": "اشتري لي القهوة!",
"github_title": "PairDrop على جيت هاب",
"faq_title": "أسئلة متكررة"
},
"document-titles": {
"file-transfer-requested": "طلب نقل الملف",
"message-received-plural": "{{count}} الرسائل المستلمة",
"message-received": "تم إرسال الرسالة",
"file-received": "تم استلام الملف",
"file-received-plural": "{{count}} الملفات المستلمة",
"image-transfer-requested": "طُلب نقل الصور المطلوبة"
}
}

167
public/lang/de.json Normal file
View File

@@ -0,0 +1,167 @@
{
"header": {
"about_title": "Über PairDrop",
"notification_title": "Benachrichtigungen aktivieren",
"about_aria-label": "Über PairDrop öffnen",
"install_title": "PairDrop installieren",
"pair-device_title": "Deine Geräte dauerhaft koppeln",
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
"theme-auto_title": "Systemstil verwenden",
"theme-dark_title": "Immer dunklen Stil verwenden",
"theme-light_title": "Immer hellen Stil verwenden",
"cancel-paste-mode": "Fertig",
"language-selector_title": "Sprache Wählen",
"join-public-room_title": "Öffentlichen Raum temporär betreten"
},
"dialogs": {
"share": "Teilen",
"download": "Herunterladen",
"pair-devices-title": "Geräte Dauerhaft Koppeln",
"input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein",
"enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.",
"pair": "Koppeln",
"cancel": "Abbrechen",
"edit-paired-devices-title": "Gekoppelte Geräte Bearbeiten",
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
"close": "Schließen",
"accept": "Akzeptieren",
"decline": "Ablehnen",
"title-image": "Bild",
"title-file": "Datei",
"title-image-plural": "Bilder",
"title-file-plural": "Dateien",
"scan-qr-code": "oder scanne den QR-Code.",
"would-like-to-share": "möchte Folgendes teilen",
"send": "Senden",
"copy": "Kopieren",
"receive-text-title": "Textnachricht Erhalten",
"file-other-description-image-plural": "und {{count}} andere Bilder",
"file-other-description-file-plural": "und {{count}} andere Dateien",
"auto-accept-instructions-1": "Aktiviere",
"auto-accept": "automatisch-akzeptieren",
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
"has-sent": "hat Folgendes gesendet:",
"send-message-title": "Textnachricht Senden",
"send-message-to": "Sende eine Textnachricht an",
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
"base64-text": "Text",
"base64-files": "Dateien",
"base64-processing": "Bearbeitung läuft…",
"file-other-description-image": "und ein anderes Bild",
"file-other-description-file": "und eine andere Datei",
"receive-title": "{{descriptor}} Erhalten",
"download-again": "Erneut herunterladen",
"system-language": "Systemsprache",
"language-selector-title": "Sprache Einstellen",
"hr-or": "ODER",
"input-room-id-on-another-device": "Gib diese Raum-ID auf einem anderen Gerät ein",
"unpair": "Entkoppeln",
"leave": "Verlassen",
"join": "Betreten",
"enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.",
"temporary-public-room-title": "Temporärer Öffentlicher Raum",
"message_title": "Nachricht zum Senden hier einfügen",
"pair-devices-qr-code_title": "Klicke, um Link zum Koppeln mit diesem Gerät zu kopieren",
"public-room-qr-code_title": "Klicke, um Link zu diesem öffentlichen Raum zu kopieren"
},
"about": {
"tweet_title": "Über PairDrop twittern",
"faq_title": "Häufig gestellte Fragen",
"close-about_aria-label": "Schließe Über PairDrop",
"github_title": "PairDrop auf GitHub",
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
"claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen"
},
"footer": {
"known-as": "Du wirst angezeigt als:",
"display-name_title": "Setze einen permanenten Gerätenamen",
"on-this-network": "in diesem Netzwerk",
"paired-devices": "für gekoppelte Geräte",
"traffic": "Datenverkehr wird",
"display-name_placeholder": "Lade…",
"routed": "durch den Server geleitet",
"webrtc": "wenn WebRTC nicht verfügbar ist.",
"display-name_data-placeholder": "Lade…",
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum gefunden werden, unabhängig von deinem Netzwerk.",
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
"public-room-devices": "in Raum {{roomId}}",
"discovery": "Du bist sichtbar:",
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
},
"notifications": {
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
"click-to-download": "Klicken zum Download",
"copied-text": "Text in die Zwischenablage kopiert",
"connected": "Verbunden.",
"pairing-success": "Geräte gekoppelt.",
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
"pairing-key-invalid": "Ungültiger Schlüssel",
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
"copied-to-clipboard": "In die Zwischenablage kopiert",
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
"notifications-enabled": "Benachrichtigungen aktiviert.",
"offline": "Du bist offline",
"online": "Du bist wieder Online",
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
"download-successful": "{{descriptor}} heruntergeladen",
"pairing-cleared": "Alle Geräte entkoppelt.",
"click-to-show": "Klicken zum Anzeigen",
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert.",
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
"connecting": "Verbindung wird hergestellt…",
"files-incorrect": "Dateien sind fehlerhaft.",
"file-transfer-completed": "Dateiübertragung abgeschlossen.",
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
"public-room-id-invalid": "Ungültige Raum-ID",
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können.",
"notifications-permissions-error": "Benachrichtigungen wurden blockiert, weil der Nutzer die Berechtigungsanfrage mehrfach abgelehnt hat. Dies kann in den Einstellungen der Website zurückgesetzt werden, welche durch Klick auf das Schloss Symbol neben der URL Leiste erreicht werden können.",
"pair-url-copied-to-clipboard": "Link zum Koppeln mit diesem Gerät in Zwischenablage kopiert",
"room-url-copied-to-clipboard": "Link zu diesem öffentlichen Raum in Zwischenablage kopiert"
},
"instructions": {
"x-instructions_desktop": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
"click-to-send": "Klicke zum Senden von",
"tap-to-send": "Tippe zum Senden von",
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
"x-instructions_mobile": "Tippe, um Dateien zu senden oder tippe lange, um Nachrichten zu senden",
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
"activate-paste-mode-shared-text": "freigegebenem Text"
},
"document-titles": {
"file-transfer-requested": "Dateiübertragung angefordert",
"file-received": "Datei erhalten",
"file-received-plural": "{{count}} Dateien erhalten",
"message-received": "Nachricht erhalten",
"message-received-plural": "{{count}} Nachrichten erhalten",
"image-transfer-requested": "Bilder Transfer beantragt"
},
"peer-ui": {
"click-to-send": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
"waiting": "Warte…",
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
"transferring": "Übertragung läuft…",
"processing": "Bearbeitung läuft…",
"preparing": "Vorbereitung läuft…"
}
}

165
public/lang/en.json Normal file
View File

@@ -0,0 +1,165 @@
{
"header": {
"about_title": "About PairDrop",
"language-selector_title": "Set Language",
"about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt theme to system automatically",
"theme-light_title": "Always use light theme",
"theme-dark_title": "Always use dark theme",
"notification_title": "Enable notifications",
"install_title": "Install PairDrop",
"pair-device_title": "Pair your devices permanently",
"edit-paired-devices_title": "Edit paired devices",
"join-public-room_title": "Join public room temporarily",
"cancel-paste-mode": "Done"
},
"instructions": {
"no-peers_data-drop-bg": "Release to select recipient",
"no-peers-title": "Open PairDrop on other devices to send files",
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
"x-instructions_desktop": "Click to send files or right click to send a message",
"x-instructions_mobile": "Tap to send files or long tap to send a message",
"x-instructions_data-drop-peer": "Release to send to peer",
"x-instructions_data-drop-bg": "Release to select recipient",
"click-to-send": "Click to send",
"tap-to-send": "Tap to send",
"activate-paste-mode-base": "Open PairDrop on other devices to send",
"activate-paste-mode-and-other-files": "and {{count}} other files",
"activate-paste-mode-shared-text": "shared text"
},
"footer": {
"known-as": "You are known as:",
"display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently",
"discovery": "You can be discovered:",
"on-this-network": "on this network",
"on-this-network_title": "You can be discovered by everyone on this network.",
"paired-devices": "by paired devices",
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
"public-room-devices": "in room {{roomId}}",
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
"traffic": "Traffic is",
"routed": "routed through the server",
"webrtc": "if WebRTC is not available."
},
"dialogs": {
"pair-devices-title": "Pair Devices Permanently",
"input-key-on-this-device": "Input this key on another device",
"scan-qr-code": "or scan the QR-code.",
"enter-key-from-another-device": "Enter key from another device here.",
"temporary-public-room-title": "Temporary Public Room",
"input-room-id-on-another-device": "Input this room ID on another device",
"enter-room-id-from-another-device": "Enter room ID from another device to join room.",
"hr-or": "OR",
"pair": "Pair",
"cancel": "Cancel",
"edit-paired-devices-title": "Edit Paired Devices",
"unpair": "Unpair",
"paired-devices-wrapper_data-empty": "No paired devices.",
"auto-accept-instructions-1": "Activate",
"auto-accept": "auto-accept",
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
"close": "Close",
"join": "Join",
"leave": "Leave",
"would-like-to-share": "would like to share",
"accept": "Accept",
"decline": "Decline",
"has-sent": "has sent:",
"share": "Share",
"download": "Download",
"send-message-title": "Send Message",
"send-message-to": "Send a Message to",
"message_title": "Insert message to send",
"send": "Send",
"receive-text-title": "Message Received",
"copy": "Copy",
"base64-processing": "Processing…",
"base64-tap-to-paste": "Tap here to paste {{type}}",
"base64-paste-to-send": "Paste here to send {{type}}",
"base64-text": "text",
"base64-files": "files",
"file-other-description-image": "and 1 other image",
"file-other-description-file": "and 1 other file",
"file-other-description-image-plural": "and {{count}} other images",
"file-other-description-file-plural": "and {{count}} other files",
"title-image": "Image",
"title-file": "File",
"title-image-plural": "Images",
"title-file-plural": "Files",
"receive-title": "{{descriptor}} Received",
"download-again": "Download again",
"language-selector-title": "Set Language",
"system-language": "System Language",
"public-room-qr-code_title": "Click to copy link to public room",
"pair-devices-qr-code_title": "Click to copy link to pair this device"
},
"about": {
"close-about_aria-label": "Close About PairDrop",
"claim": "The easiest way to transfer files across devices",
"github_title": "PairDrop on GitHub",
"buy-me-a-coffee_title": "Buy me a coffee!",
"tweet_title": "Tweet about PairDrop",
"faq_title": "Frequently asked questions"
},
"notifications": {
"display-name-changed-permanently": "Display name is changed permanently.",
"display-name-changed-temporarily": "Display name is changed only for this session.",
"display-name-random-again": "Display name is randomly generated again.",
"download-successful": "{{descriptor}} downloaded",
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
"pairing-success": "Devices paired.",
"pairing-not-persistent": "Paired devices are not persistent.",
"pairing-key-invalid": "Invalid key",
"pairing-key-invalidated": "Key {{key}} invalidated.",
"pairing-cleared": "All Devices unpaired.",
"public-room-id-invalid": "Invalid room ID",
"public-room-left": "Left public room {{publicRoomId}}",
"copied-to-clipboard": "Copied to clipboard",
"pair-url-copied-to-clipboard": "Link to pair this device copied to clipboard",
"room-url-copied-to-clipboard": "Link to public room copied to clipboard",
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
"text-content-incorrect": "Text content is incorrect.",
"file-content-incorrect": "File content is incorrect.",
"clipboard-content-incorrect": "Clipboard content is incorrect.",
"notifications-enabled": "Notifications enabled.",
"notifications-permissions-error": "Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL bar.",
"link-received": "Link received by {{name}} - Click to open",
"message-received": "Message received by {{name}} - Click to copy",
"click-to-download": "Click to download",
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
"click-to-show": "Click to show",
"copied-text": "Copied text to clipboard",
"copied-text-error": "Writing to clipboard failed. Copy manually!",
"offline": "You are offline",
"online": "You are back online",
"connected": "Connected.",
"online-requirement-pairing": "You need to be online to pair devices.",
"online-requirement-public-room": "You need to be online to create a public room.",
"connecting": "Connecting…",
"files-incorrect": "Files are incorrect.",
"file-transfer-completed": "File transfer completed.",
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
"message-transfer-completed": "Message transfer completed.",
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
"selected-peer-left": "Selected peer left."
},
"document-titles": {
"file-received": "File Received",
"file-received-plural": "{{count}} Files Received",
"file-transfer-requested": "File Transfer Requested",
"image-transfer-requested": "Image Transfer Requested",
"message-received": "Message Received",
"message-received-plural": "{{count}} Messages Received"
},
"peer-ui": {
"click-to-send-paste-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"preparing": "Preparing…",
"waiting": "Waiting…",
"processing": "Processing…",
"transferring": "Transferring…"
}
}

165
public/lang/es.json Normal file
View File

@@ -0,0 +1,165 @@
{
"header": {
"theme-auto_title": "Adaptar tema al sistema",
"language-selector_title": "Configurar Idioma",
"about_title": "Sobre PairDrop",
"about_aria-label": "Abrir Sobre PairDrop",
"cancel-paste-mode": "Listo",
"install_title": "Instalar PairDrop",
"theme-dark_title": "Siempre usar tema oscuro",
"pair-device_title": "Empareja tus dispositivos permanentemente",
"join-public-room_title": "Unirse a una sala pública temporalmente",
"notification_title": "Activar notificaciones",
"edit-paired-devices_title": "Editar dispositivos emparejados",
"theme-light_title": "Siempre usar tema claro"
},
"footer": {
"webrtc": "si WebRTC no está disponible.",
"public-room-devices_title": "Puedes ser descubierto por dispositivos en esta sala pública independientemente de la red.",
"display-name_data-placeholder": "Cargando…",
"display-name_title": "Edita el nombre de tu dispositivo de forma permanente",
"traffic": "El tráfico es",
"paired-devices_title": "Puedes ser descubierto por los dispositivos emparejados todo el tiempo independientemente de la red.",
"public-room-devices": "en la sala {{roomId}}",
"paired-devices": "por dispositivos emparejados",
"on-this-network": "en esta red",
"routed": "enrutado a través del servidor",
"discovery": "Puedes ser descubierto:",
"on-this-network_title": "Puedes ser descubierto por todos en esta red.",
"known-as": "Eres conocido como:"
},
"notifications": {
"request-title": "{{name}} quiere transferir {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Hay transferencias no terminadas. ¿Estás seguro de que quieres cerrar PairDrop?",
"message-received": "Mensaje recibido por {{name}} - Haga clic para copiar",
"rate-limit-join-key": "Límite de intentos alcanzado. Espere 10 segundos y vuelva a intentarlo.",
"connecting": "Conectando…",
"pairing-key-invalidated": "Clave {{key}} invalidada.",
"pairing-key-invalid": "Clave inválida",
"connected": "Connectado.",
"pairing-not-persistent": "Los dispositivos emparejados no son persistentes.",
"text-content-incorrect": "El contenido del texto es incorrecto.",
"message-transfer-completed": "Transferencia de mensaje completada.",
"file-transfer-completed": "Transferencia de archivos completada.",
"file-content-incorrect": "El contenido del archivo es incorrecto.",
"files-incorrect": "Los archivos son incorrectos.",
"selected-peer-left": "El dispositivo seleccionado se fue.",
"link-received": "Link recibido por {{name}} - Haga clic para abrir",
"online": "Estás de nuevo en línea",
"public-room-left": "Salió de la sala pública {{publicRoomId}}",
"copied-text": "Texto copiado al portapapeles",
"display-name-random-again": "El nombre mostrado se genera aleatoriamente nuevamente.",
"display-name-changed-permanently": "El nombre para mostrar se ha cambiado permanentemente.",
"copied-to-clipboard-error": "No es posible copiarlo. Cópielo manualmente.",
"pairing-success": "Dispositivos emparejados.",
"clipboard-content-incorrect": "El contenido del portapapeles es incorrecto.",
"display-name-changed-temporarily": "El nombre mostrado se cambia solo para esta sesión.",
"copied-to-clipboard": "Copiado al portapapeles",
"offline": "Estás desconectado",
"pairing-tabs-error": "Emparejar dos pestañas del navegador es imposible.",
"public-room-id-invalid": "ID de sala no válido",
"click-to-download": "Haga clic para descargar",
"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",
"click-to-show": "Click para mostrar",
"notifications-permissions-error": "Las notificaciones se bloquearon porque el usuario rechazó la solicitud del permiso varias veces. Esto se puede restablecer en la configuración de la página web, a la que se quiere acceder haciendo clic en el icono del candado al lado de la barra con la URL.",
"pair-url-copied-to-clipboard": "El enlace para emparejar este dispositivo se copió en el portapapeles",
"room-url-copied-to-clipboard": "El enlace a la sala pública se copió en el portapapeles"
},
"instructions": {
"x-instructions_mobile": "Toque para enviar archivos o toque prologádamente para enviar un mensaje",
"click-to-send": "Haga clic para enviar",
"activate-paste-mode-and-other-files": "y {{count}} archivos diferentes",
"tap-to-send": "Toca para enviar",
"activate-paste-mode-base": "Abra PairDrop en otros dispositivos para enviar",
"no-peers-subtitle": "Empareje dispositivos o ingrese a una sala pública para que lo puedan encontrar en otras redes",
"activate-paste-mode-shared-text": "texto compartido",
"x-instructions_desktop": "Haga clic para enviar archivos o haga clic derecho para enviar un mensaje",
"no-peers-title": "Abra PairDrop en otros dispositivos para enviar archivos",
"x-instructions_data-drop-peer": "Liberar para enviar a un par",
"x-instructions_data-drop-bg": "Liberar para seleccionar destinatario",
"no-peers_data-drop-bg": "Liberar para seleccionar destinatario"
},
"peer-ui": {
"processing": "Procesando…",
"click-to-send-paste-mode": "Haga clic para enviar {{descriptor}}",
"click-to-send": "Haga clic para enviar archivos o haga clic derecho para enviar un mensaje",
"waiting": "Esperando…",
"connection-hash": "Para verificar la seguridad del cifrado de extremo a extremo, compare este número de seguridad en ambos dispositivos",
"preparing": "Preparando…",
"transferring": "Transferiendo…"
},
"dialogs": {
"base64-paste-to-send": "Pegar aquí para enviar {{type}}",
"auto-accept-instructions-2": "para aceptar automáticamente todos los archivos enviados desde ese dispositivo.",
"receive-text-title": "Mensaje Recibido",
"edit-paired-devices-title": "Editar Dispositivos Emparejados",
"cancel": "Cancelar",
"auto-accept-instructions-1": "Activar",
"pair-devices-title": "Emparejar dispositivos permanentemente",
"download": "Descargar",
"title-file": "Archivo",
"base64-processing": "Procesando…",
"decline": "Rechazar",
"receive-title": "{{descriptor}} Recibido",
"leave": "Salir",
"join": "Unirse",
"title-image-plural": "Imágenes",
"send": "Enviar",
"base64-tap-to-paste": "Toca aquí para pegar {{type}}",
"base64-text": "texto",
"copy": "Copiar",
"file-other-description-image": "y una imagen mas",
"temporary-public-room-title": "Sala pública temporal",
"base64-files": "archivos",
"has-sent": "ha enviado:",
"file-other-description-file": "y otro archivo",
"close": "Cerrar",
"system-language": "Idioma del Sistema",
"unpair": "Desemparejar",
"title-image": "Imagen",
"file-other-description-file-plural": "y {{count}} archivos más",
"would-like-to-share": "quisiera compartir",
"send-message-to": "Enviar un Mensaje a",
"language-selector-title": "Configurar Idioma",
"pair": "Emparejar",
"hr-or": "O",
"scan-qr-code": "o escanea el código QR.",
"input-key-on-this-device": "Ingrese esta clave en otro dispositivo",
"download-again": "Descargar de nuevo",
"accept": "Aceptar",
"paired-devices-wrapper_data-empty": "Sin dispositivos emparejados.",
"enter-key-from-another-device": "Ingresa la clave de otro dispositivo aquí.",
"share": "Compartir",
"auto-accept": "aceptar automáticamente",
"title-file-plural": "Archivos",
"send-message-title": "Enviar Mensaje",
"input-room-id-on-another-device": "Ingrese el ID de esta sala en otro dispositivo",
"file-other-description-image-plural": "y {{count}} imágenes más",
"enter-room-id-from-another-device": "Ingresa el ID de la sala desde otro dispositivo para unirte a la sala.",
"message_title": "Insertar el mensaje a enviar",
"pair-devices-qr-code_title": "Haz clic para copiar el enlace para emparejar este dispositivo",
"public-room-qr-code_title": "Haz clic para copiar el enlace a la sala pública"
},
"about": {
"claim": "La forma más sencilla de transferir archivos entre dispositivos",
"tweet_title": "Tweetea sobre PairDrop",
"close-about_aria-label": "Cerrar Sobre PairDrop",
"buy-me-a-coffee_title": "¡Cómprame un café!",
"github_title": "PairDrop en GitHub",
"faq_title": "Preguntas frecuentes"
},
"document-titles": {
"file-transfer-requested": "Transferencia de archivos solicitada",
"image-transfer-requested": "Transferencia de imagen solicitada",
"message-received-plural": "{{count}} Mensajes recibidos",
"message-received": "Mensaje recibido",
"file-received": "Archivo Recibido",
"file-received-plural": "{{count}} Archivos Recibidos"
}
}

160
public/lang/fr.json Normal file
View File

@@ -0,0 +1,160 @@
{
"header": {
"about_title": "À propos de PairDrop",
"language-selector_title": "Choix de la langue",
"about_aria-label": "Ouvrir à propos de PairDrop",
"theme-auto_title": "Adapter le thème au système",
"theme-light_title": "Toujours utiliser le thème clair",
"theme-dark_title": "Toujours utiliser le thème sombre",
"notification_title": "Activer les notifications",
"install_title": "Installer PairDrop",
"pair-device_title": "Associez vos appareils de manière permanente",
"edit-paired-devices_title": "Gérer les appareils couplés",
"join-public-room_title": "Rejoindre temporairement la salle publique",
"cancel-paste-mode": "Terminé"
},
"instructions": {
"no-peers_data-drop-bg": "Déposer pour choisir le destinataire",
"no-peers-title": "Ouvrez PairDrop sur d'autres appareils pour envoyer des fichiers",
"no-peers-subtitle": "Associez des appareils ou entrez dans une salle publique pour être visible sur d'autres réseaux",
"x-instructions_desktop": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
"x-instructions_mobile": "Appuyez pour envoyer des fichiers ou appuyez longuement pour envoyer un message",
"x-instructions_data-drop-peer": "Déposer pour envoyer au destinataire",
"x-instructions_data-drop-bg": "Lâcher pour choisir le destinataire",
"click-to-send": "Cliquez pour envoyer",
"tap-to-send": "Appuyez pour envoyer",
"activate-paste-mode-base": "Ouvrez PairDrop sur d'autres appareils pour envoyer",
"activate-paste-mode-and-other-files": "et {{count}} autres fichiers",
"activate-paste-mode-shared-text": "texte partagé"
},
"footer": {
"known-as": "Vous êtes connu comme :",
"display-name_data-placeholder": "Chargement…",
"display-name_title": "Modifiez le nom de votre appareil de manière permanente",
"discovery": "Vous pouvez être découvert :",
"on-this-network": "sur ce réseau",
"on-this-network_title": "Vous pouvez être découvert par tout le monde sur ce réseau.",
"paired-devices": "par les appareils couplés",
"paired-devices_title": "Vous pouvez être découvert par les appareils couplés à tout moment, indépendamment du réseau.",
"public-room-devices": "dans la salle {{roomId}}",
"public-room-devices_title": "Vous pouvez être découvert par les appareils de cette salle publique indépendamment du réseau.",
"traffic": "Le trafic est",
"routed": "routé via le serveur",
"webrtc": "si WebRTC n'est pas disponible.",
"display-name_placeholder": "Chargement…"
},
"dialogs": {
"pair-devices-title": "Associer les appareils de manière permanente",
"input-key-on-this-device": "Saisissez cette clé sur un autre appareil",
"scan-qr-code": "ou scannez le QR-code.",
"enter-key-from-another-device": "Entrez ici la clé d'un autre appareil.",
"temporary-public-room-title": "Salle publique temporaire",
"input-room-id-on-another-device": "Saisissez cet ID de salle sur un autre appareil",
"enter-room-id-from-another-device": "Entrez l'ID de la salle depuis un autre appareil pour rejoindre la salle.",
"hr-or": "OU",
"pair": "associer",
"cancel": "Annuler",
"edit-paired-devices-title": "Modifier les appareils couplés",
"unpair": "Dissocier",
"paired-devices-wrapper_data-empty": "Aucun appareil couplé.",
"auto-accept-instructions-1": "Activer",
"auto-accept": "auto-accepter",
"auto-accept-instructions-2": "pour accepter automatiquement tous les fichiers envoyés depuis cet appareil.",
"close": "Fermer",
"join": "Rejoindre",
"leave": "Partir",
"would-like-to-share": "aimerait partager",
"accept": "Accepter",
"decline": "Refuser",
"has-sent": "a envoyé :",
"share": "Partage",
"download": "Télécharger",
"send-message-title": "Envoyer un message",
"send-message-to": "Envoyer un message à",
"send": "Envoyer",
"receive-text-title": "Message reçu",
"copy": "Copier",
"base64-processing": "Traitement…",
"base64-tap-to-paste": "Appuyez ici pour coller {{type}}",
"base64-paste-to-send": "Coller ici pour envoyer {{type}}",
"base64-text": "texte",
"base64-files": "fichiers",
"file-other-description-image": "et 1 autre image",
"file-other-description-file": "et 1 autre fichier",
"file-other-description-image-plural": "et {{count}} autres images",
"file-other-description-file-plural": "et {{count}} autres fichiers",
"title-image": "Image",
"title-file": "Fichier",
"title-image-plural": "Images",
"title-file-plural": "Fichiers",
"receive-title": "{{descriptor}} Reçu",
"download-again": "Télécharger à nouveau",
"language-selector-title": "Définir la langue",
"system-language": "Langue du système"
},
"about": {
"close-about_aria-label": "Fermer à propos de PairDrop",
"claim": "Le moyen le plus simple de transférer des fichiers entre appareils",
"github_title": "PairDrop sur GitHub",
"buy-me-a-coffee_title": "Acheté-moi un café!",
"tweet_title": "Tweet à propos de PairDrop",
"faq_title": "Questions fréquemment posées"
},
"notifications": {
"display-name-changed-permanently": "Le nom d'affichage est modifié de manière permanente.",
"display-name-changed-temporarily": "Le nom d'affichage est modifié uniquement pour cette session.",
"display-name-random-again": "Le nom d'affichage est à nouveau généré aléatoirement.",
"download-successful": "{{descriptor}} téléchargé",
"pairing-tabs-error": "Le couplage de deux onglets de navigateur Web est impossible.",
"pairing-success": "Appareils couplés.",
"pairing-not-persistent": "Les appareils couplés ne sont pas persistants.",
"pairing-key-invalid": "Clé invalide",
"pairing-key-invalidated": "Clé {{key}} invalidée.",
"pairing-cleared": "Tous les appareils ne sont plus appairés.",
"public-room-id-invalid": "ID de salle non valide",
"public-room-left": "Salle publique {{publicRoomId}} quittée",
"copied-to-clipboard": "Copié dans le presse-papier",
"copied-to-clipboard-error": "Copie impossible. Copier manuellement.",
"text-content-incorrect": "Le contenu du texte est incorrect.",
"file-content-incorrect": "Le contenu du fichier est incorrect.",
"clipboard-content-incorrect": "Le contenu du presse-papiers est incorrect.",
"notifications-enabled": "Notifications activées.",
"link-received": "Lien reçu par {{name}} - Cliquez pour ouvrir",
"message-received": "Message reçu par {{name}} - Cliquez pour copier",
"click-to-download": "Cliquez pour télécharger",
"request-title": "{{name}} souhaite transférer {{count}} {{descriptor}}",
"click-to-show": "Cliquez pour afficher",
"copied-text": "Texte copié dans le presse-papiers",
"copied-text-error": "L'écriture dans le presse-papiers a échoué. Copiez manuellement !",
"offline": "Vous êtes hors ligne",
"online": "Vous êtes de nouveau en ligne",
"connected": "Connecté.",
"online-requirement-pairing": "Vous devez être en ligne pour coupler des appareils.",
"online-requirement-public-room": "Vous devez être en ligne pour créer une salle publique.",
"connecting": "Connexion…",
"files-incorrect": "Les fichiers sont incorrects.",
"file-transfer-completed": "Transfert de fichier terminé.",
"ios-memory-limit": "L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois",
"message-transfer-completed": "Transfert de message terminé.",
"unfinished-transfers-warning": "Il y a des transferts inachevés. Êtes-vous sûr de vouloir fermer PairDrop ?",
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",
"selected-peer-left": "Appareils sélectionnés restants."
},
"document-titles": {
"file-received": "Fichier reçu",
"file-received-plural": "{{count}} fichiers reçus",
"file-transfer-requested": "Transfert de fichier demandé",
"image-transfer-requested": "Transfert d'image demandé",
"message-received": "Message reçu",
"message-received-plural": "{{count}} Messages reçus"
},
"peer-ui": {
"click-to-send-paste-mode": "Cliquez pour envoyer {{descriptor}}",
"click-to-send": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
"connection-hash": "Pour vérifier la sécurité du chiffrement de bout en bout, comparez ce numéro de sécurité sur les deux appareils",
"preparing": "Préparation…",
"waiting": "En attente…",
"processing": "En cours…",
"transferring": "Transfert en cours…"
}
}

165
public/lang/id.json Normal file
View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "jika WebRTC tidak tersedia.",
"public-room-devices_title": "Anda dapat ditemukan oleh perangkat di ruang publik ini terlepas dari jaringan.",
"display-name_data-placeholder": "Memuat…",
"display-name_title": "Edit nama perangkat Anda scr. permanen",
"traffic": "Lalu lintas",
"paired-devices_title": "Anda dapat ditemukan oleh perangkat yang dipasangkan setiap saat tergantung pada jaringan.",
"public-room-devices": "dalam room {{roomId}}",
"paired-devices": "pada prngkt. yg. dipasangkan",
"on-this-network": "pada jaringan ini",
"routed": "diarahkan melalui server",
"discovery": "Anda dapat ditemukan:",
"on-this-network_title": "Anda dapat ditemukan oleh semua orang di jaringan ini.",
"known-as": "Anda dikenal sebagai:"
},
"notifications": {
"request-title": "{{name}} ingin mentransfer {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Ada transfer yang belum selesai. Apakah Anda yakin ingin menutup PairDrop?",
"message-received": "Pesan diterima dari {{name}} - Klik untuk menyalin",
"rate-limit-join-key": "Batasan tercapai. Tunggu 10 detik dan coba lagi.",
"connecting": "Menghubungkan…",
"pairing-key-invalidated": "Kunci {{key}} tidak valid.",
"pairing-key-invalid": "Kunci tidak valid",
"connected": "Tersambung.",
"pairing-not-persistent": "Perangkat dipasangkan tidak akan bertahan lama.",
"text-content-incorrect": "Isi teks keliru.",
"message-transfer-completed": "Transfer pesan selesai.",
"file-transfer-completed": "Transfer file selesai.",
"file-content-incorrect": "Isi file keliru.",
"files-incorrect": "File tidak benar.",
"selected-peer-left": "Rekan terpilih keluar.",
"link-received": "Tautan diterima dari {{name}} - Klik untuk membuka",
"online": "Anda kembali online",
"public-room-left": "Keluar dari ruang publik {{publicRoomId}}",
"copied-text": "Teks disalin ke papan klip",
"display-name-random-again": "Nama tampilan dibuat secara acak lagi.",
"display-name-changed-permanently": "Nama tampilan diubah secara permanen.",
"copied-to-clipboard-error": "Penyalinan tak dapat dilakukan. Salinlah secara manual.",
"pairing-success": "Perangkat dipasangkan.",
"clipboard-content-incorrect": "Isi papan klip keliru.",
"display-name-changed-temporarily": "Nama tampilan hanya diubah untuk sesi ini.",
"copied-to-clipboard": "Disalin ke papan klip",
"offline": "Anda sedang offline",
"pairing-tabs-error": "Memasangkan dua tab browser web tidak mungkin dilakukan.",
"public-room-id-invalid": "Room ID tidak valid",
"click-to-download": "Klik untuk mengunduh",
"pairing-cleared": "Semua Perangkat dilepaskan.",
"notifications-enabled": "Notifikasi diaktifkan.",
"online-requirement-pairing": "Anda harus online untuk memasangkan perangkat.",
"ios-memory-limit": "Mengirim file ke iOS hanya dapat dilakukan hingga 200 MB sekaligus",
"online-requirement-public-room": "Anda harus online untuk membuat ruang publik.",
"copied-text-error": "Menyalin ke papan klip gagal. Salinlah secara manual!",
"download-successful": "{{descriptor}} diunduh",
"click-to-show": "Klik untuk menampilkan",
"notifications-permissions-error": "Izin pemberitahuan telah diblokir karena pengguna telah mengabaikan permintaan izin beberapa kali. Hal ini dapat diatur ulang di Info Halaman yang dapat diakses dengan mengeklik ikon kunci di sebelah bar URL.",
"pair-url-copied-to-clipboard": "Tautan untuk memasangkan perangkat ini disalin ke papan klip",
"room-url-copied-to-clipboard": "Tautan ke ruang publik disalin ke papan klip"
},
"header": {
"cancel-paste-mode": "Selesai",
"theme-auto_title": "Sesuaikan tema dengan sistem",
"install_title": "Instal PairDrop",
"theme-dark_title": "Selalu gunakan tema gelap",
"pair-device_title": "Pasangkan perangkat anda secara permanen",
"join-public-room_title": "Bergabung dgn. ruang publik sementara",
"notification_title": "Aktifkan notifikasi",
"edit-paired-devices_title": "Edit perangkat yg. dipasangkan",
"language-selector_title": "Atur Bahasa",
"about_title": "Tentang PairDrop",
"about_aria-label": "Buka Tentang PairDrop",
"theme-light_title": "Selalu gunakan tema terang"
},
"instructions": {
"x-instructions_mobile": "Ketuk untuk mengirim file atau ketuk lama untuk mengirim pesan",
"click-to-send": "Klik untuk mengirim",
"activate-paste-mode-and-other-files": "dan {{count}} file lainnya",
"tap-to-send": "Ketuk untuk mengirim",
"activate-paste-mode-base": "Buka PairDrop di perangkat lain untuk berkirim",
"no-peers-subtitle": "Pasangkan perangkat atau masuk ke ruang publik agar dapat terdeteksi di jaringan lain",
"activate-paste-mode-shared-text": "teks bersama",
"x-instructions_desktop": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
"no-peers-title": "Buka PairDrop di perangkat lain untuk berkirim file",
"x-instructions_data-drop-peer": "Lepaskan untuk mengirim ke rekan",
"x-instructions_data-drop-bg": "Lepaskan untuk memilih penerima",
"no-peers_data-drop-bg": "Lepaskan untuk memilih penerima"
},
"peer-ui": {
"processing": "Memproses…",
"click-to-send-paste-mode": "Klik untuk mengirim {{descriptor}}",
"click-to-send": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
"waiting": "Menunggu…",
"connection-hash": "Untuk memverifikasi keamanan enkripsi end-to-end, bandingkan nomor keamanan ini pada kedua perangkat",
"preparing": "Menyiapkan…",
"transferring": "Mentransfer…"
},
"dialogs": {
"base64-paste-to-send": "Tempel di sini untuk mengirim {{type}}",
"auto-accept-instructions-2": "untuk secara otomatis menerima semua file yang dikirim dari perangkat tersebut.",
"receive-text-title": "Pesan Diterima",
"edit-paired-devices-title": "Edit Perangkat yg. Dipasangkan",
"cancel": "Batal",
"auto-accept-instructions-1": "Aktifkan",
"pair-devices-title": "Pasangkan Perangkat Scr. Permanen",
"download": "Unduh",
"title-file": "File",
"base64-processing": "Memproses…",
"decline": "Tolak",
"receive-title": "{{descriptor}} Diterima",
"leave": "Tinggalkan",
"join": "Gabung",
"title-image-plural": "Gambar",
"send": "Kirim",
"base64-tap-to-paste": "Ketuk di sini untuk menempelkan {{type}}",
"base64-text": "teks",
"copy": "Salin",
"file-other-description-image": "dan 1 gambar lainnya",
"temporary-public-room-title": "Ruang Publik Sementara",
"base64-files": "file",
"has-sent": "telah mengirim:",
"file-other-description-file": "dan 1 file lainnya",
"close": "Tutup",
"system-language": "Bahasa Sistem",
"unpair": "Lepas",
"title-image": "Gambar",
"file-other-description-file-plural": "dan {{count}} file lainnya",
"would-like-to-share": "ingin berbagi",
"send-message-to": "Kirim pesan ke",
"language-selector-title": "Pilih Bahasa",
"pair": "Pasangkan",
"hr-or": "ATAU",
"scan-qr-code": "atau pindai kode QR.",
"input-key-on-this-device": "Masukkan kunci ini pada perangkat lain",
"download-again": "Unduh lagi",
"accept": "Terima",
"paired-devices-wrapper_data-empty": "Tak ada perangkat yg. dipasangkan.",
"enter-key-from-another-device": "Masukkan kunci dari perangkat lain di sini.",
"share": "Bagikan",
"auto-accept": "terima-otomatis",
"title-file-plural": "File",
"send-message-title": "Kirim Pesan",
"input-room-id-on-another-device": "Masukkan room ID ini pada perangkat lain",
"file-other-description-image-plural": "dan {{count}} gambar lainnya",
"enter-room-id-from-another-device": "Masukkan room ID dari perangkat lain untuk bergabung dengan room.",
"message_title": "Masukkan pesan untuk dikirim",
"pair-devices-qr-code_title": "Klik untuk menyalin tautan untuk memasangkan perangkat ini",
"public-room-qr-code_title": "Klik untuk menyalin tautan ke ruang publik"
},
"about": {
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
"tweet_title": "Tweet tentang PairDrop",
"close-about_aria-label": "Tutup Tentang PairDrop",
"buy-me-a-coffee_title": "Traktir aku kopi!",
"github_title": "PairDrop di GitHub",
"faq_title": "Pertanyaan yang sering diajukan"
},
"document-titles": {
"file-transfer-requested": "Permintaan Transfer File",
"message-received-plural": "{{count}} Pesan Diterima",
"message-received": "Pesan Diterima",
"file-received": "File Diterima",
"file-received-plural": "{{count}} File Diterima",
"image-transfer-requested": "Permintaan Transfer Gambar"
}
}

165
public/lang/it.json Normal file
View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "se WebRTC non è disponibile.",
"public-room-devices_title": "Puoi essere rilevato dai dispositivi presenti in questa stanza pubblica indipendentemente dalla rete.",
"display-name_data-placeholder": "Caricamento…",
"display-name_title": "Modifica il nome del tuo dispositivo permanentemente",
"traffic": "Il traffico è",
"paired-devices_title": "Puoi essere rilevato dai dispositivi abbinati in ogni momento, indipendentemente dalla rete.",
"public-room-devices": "nella stanza {{roomId}}",
"paired-devices": "da dispositivi abbinati",
"on-this-network": "su questa rete",
"routed": "instradato attraverso il server",
"discovery": "Puoi essere rilevato:",
"on-this-network_title": "puoi essere rilevato da chiunque su questa rete.",
"known-as": "Sei visibile come:"
},
"header": {
"cancel-paste-mode": "Fatto",
"theme-auto_title": "Adatta il tema al sistema automaticamente",
"install_title": "Installa PairDrop",
"theme-dark_title": "Usa sempre il tema scuro",
"pair-device_title": "Abbina i tuoi dispositivi permanentemente",
"join-public-room_title": "Unisciti ad una stanza pubblica temporaneamente",
"notification_title": "Attiva notifiche",
"edit-paired-devices_title": "Modifica i dispositivi abbinati",
"language-selector_title": "Imposta Lingua",
"about_title": "Informazioni su PairDrop",
"about_aria-label": "Apri Informazioni su PairDrop",
"theme-light_title": "Usa sempre il tema chiaro"
},
"instructions": {
"x-instructions_mobile": "Tocca per inviare file o tocco prolungato per inviare un messaggio",
"click-to-send": "Clicca per inviare",
"activate-paste-mode-and-other-files": "e altri {{count}} files",
"tap-to-send": "Tocca per inviare",
"activate-paste-mode-base": "Apri PairDrop su altri dispositivi per inviare",
"no-peers-subtitle": "Abbina dispositivi o entra in una stanza pubblica per essere rilevabile su altre reti",
"activate-paste-mode-shared-text": "testo condiviso",
"x-instructions_desktop": "Clicca per inviare files o usa il tasto destro per inviare un messaggio",
"no-peers-title": "Apri PairDrop su altri dispositivi per inviare files",
"x-instructions_data-drop-peer": "Rilascia per inviare al peer",
"x-instructions_data-drop-bg": "Rilascia per selezionare il destinatario",
"no-peers_data-drop-bg": "Rilascia per selezionare il destinatario"
},
"dialogs": {
"auto-accept-instructions-2": "per accettare automaticamente tutti i files inviati da quel dispositivo.",
"edit-paired-devices-title": "Modifica Dispositivi Abbinati",
"cancel": "Annulla",
"auto-accept-instructions-1": "Attiva",
"pair-devices-title": "Abbina Dispositivi Permanentemente",
"temporary-public-room-title": "Stanza Pubblica Temporanea",
"close": "Chiudi",
"unpair": "Dissocia",
"pair": "Abbina",
"scan-qr-code": "o scannerizza il codice QR.",
"input-key-on-this-device": "Inserisci questo codice su un altro dispositivo",
"paired-devices-wrapper_data-empty": "Nessun dispositivo abbinato.",
"enter-key-from-another-device": "Inserisci il codice dell'altro dispositivo qui.",
"auto-accept": "accetta-automaticamente",
"input-room-id-on-another-device": "Inserisci l'ID di questa stanza su un altro dispositivo",
"enter-room-id-from-another-device": "Inserisci l'ID stanza da un altro dispositivo per accedere alla stanza.",
"base64-paste-to-send": "Incolla qui per inviare {{type}}",
"receive-text-title": "Messaggio Ricevuto",
"download": "Scarica",
"title-file": "File",
"base64-processing": "Elaborazione…",
"decline": "Rifiuta",
"receive-title": "{{descriptor}} Ricevuto",
"leave": "Abbandona",
"join": "Unisciti",
"title-image-plural": "Immagini",
"send": "Invia",
"base64-tap-to-paste": "Tocca qui per incollare {{type}}",
"base64-text": "testo",
"copy": "Copia",
"file-other-description-image": "e 1 altra immagine",
"base64-files": "files",
"has-sent": "ha inviato:",
"file-other-description-file": "ed 1 altro file",
"system-language": "Lingua di Sistema",
"title-image": "Immagine",
"file-other-description-file-plural": "e altri {{count}} files",
"would-like-to-share": "vorrebbe condividere",
"send-message-to": "Invia un messaggio a",
"language-selector-title": "Imposta Lingua",
"hr-or": "OPPURE",
"download-again": "Scarica ancora",
"accept": "Accetta",
"share": "Condividi",
"title-file-plural": "Files",
"send-message-title": "Invia Messaggio",
"file-other-description-image-plural": "e {{count}} altre immagini",
"message_title": "Inserisci il messaggio da inviare",
"pair-devices-qr-code_title": "Clicca per copiare il link di abbinamento di questo dispositivo",
"public-room-qr-code_title": "Clicca per copirare il link della stanza pubblica"
},
"notifications": {
"request-title": "{{name}} vorrebbe trasferire {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Ci sono dei trasferimenti in corso. Sei sicuro di voler chiudere PairDrop?",
"message-received": "Messaggio ricevuto da {{name}} - Clicca per copiare",
"rate-limit-join-key": "Limite raggiunto. Aspetta 10 secondi e riprova.",
"connecting": "Connessione…",
"pairing-key-invalidated": "Il codice {{key}} è stato invalidato.",
"pairing-key-invalid": "Codice non valido",
"connected": "Connesso.",
"pairing-not-persistent": "I dispositivi abbinati non sono persistenti.",
"text-content-incorrect": "Il contenuto testuale non è corretto.",
"message-transfer-completed": "Trasferimento del messaggio completato.",
"file-transfer-completed": "Trasferimento file completato.",
"file-content-incorrect": "Il contenuto del file non è corretto.",
"files-incorrect": "I file non sono corretti.",
"selected-peer-left": "Peer selezionato ha abbandonato.",
"link-received": "Link ricevuto da {{name}} - Clicca per aprire",
"online": "Sei di nuovo online",
"public-room-left": "Ha lasciato la stanza pubblica {{publicRoomId}}",
"copied-text": "Testo copiato negli appunti",
"display-name-random-again": "Il nome visualizzato è generato casualmente un'altra volta.",
"display-name-changed-permanently": "Il nome visualizzato è cambiato permanentemente.",
"copied-to-clipboard-error": "La copia non è possibile. Copia manualmente.",
"pairing-success": "Dispositivi abbinati.",
"clipboard-content-incorrect": "Il contenuto copiato non è corretto.",
"display-name-changed-temporarily": "Il nome visualizzato è cambiato solo per questa sessione.",
"copied-to-clipboard": "Copiato negli appunti",
"offline": "Sei offline",
"pairing-tabs-error": "Abbinare due schede del browser è impossibile.",
"public-room-id-invalid": "ID stanza non valido",
"click-to-download": "Clicca per scaricare",
"pairing-cleared": "Tutti i dispositivi sono stati dissociati.",
"notifications-enabled": "Notifiche attivate.",
"online-requirement-pairing": "Devi essere online per abbinare 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",
"click-to-show": "Clicca per mostrare",
"notifications-permissions-error": "Il permesso all'invio delle notifiche è stato negato poiché l'utente ha ignorato varie volte le richieste di permesso. Ciò può essere ripristinato nelle \"informazioni sito\" cliccando sull'icona a forma di lucchetto vicino alla barra degli indirizzi.",
"pair-url-copied-to-clipboard": "Link di abbinamento copiato negli appunti",
"room-url-copied-to-clipboard": "Link della stanza copiato negli appunti"
},
"peer-ui": {
"processing": "Elaborazione…",
"click-to-send-paste-mode": "Clicca per inviare {{descriptor}}",
"click-to-send": "Clicca per inviare files o tasto destro per inviare un messaggio",
"waiting": "In attesa…",
"connection-hash": "Per verificare la sicurezza della crittografia end-to-end, confronta questo numero di sicurezza su entrambi i dispositivi",
"preparing": "Preparazione…",
"transferring": "Trasferimento…"
},
"about": {
"claim": "Il modo più semplice per trasferire files tra dispositivi",
"tweet_title": "Twitta riguardo PairDrop",
"close-about_aria-label": "Chiudi Informazioni su PairDrop",
"buy-me-a-coffee_title": "Comprami un caffè!",
"github_title": "PairDrop su GitHub",
"faq_title": "Domande Frequenti"
},
"document-titles": {
"file-transfer-requested": "Trasferimento File Richiesto",
"image-transfer-requested": "Trasferimento Immagine Richiesto",
"message-received-plural": "{{count}} Messaggi ricevuti",
"message-received": "Messaggio ricevuto",
"file-received": "File Ricevuto",
"file-received-plural": "{{count}} Files Ricevuti"
}
}

165
public/lang/ja.json Normal file
View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "WebRTCが利用できない場合。",
"public-room-devices_title": "このデバイスはネットワークと関係なく、このパブリックルームのデバイスにより発見される可能性があります。",
"display-name_data-placeholder": "読み込み中…",
"display-name_title": "永続的なデバイス名を編集する",
"traffic": "トラフィックは",
"paired-devices_title": "このデバイスはネットワークと関係なく、常にペア設定したデバイスにより発見される可能性があります。",
"public-room-devices": "ルーム{{roomId}}上",
"paired-devices": "ペア設定したデバイス",
"on-this-network": "このネットワーク上",
"routed": "サーバーを経由します",
"discovery": "このデバイスは以下で発見される可能性があります:",
"on-this-network_title": "このデバイスはこのネットワーク上の誰にでも発見される可能性があります。",
"known-as": "他のデバイスに表示される名前:"
},
"notifications": {
"request-title": "{{name}}は{{count}}個の{{descriptor}}を共有しようとしています",
"unfinished-transfers-warning": "未完了の転送があります。本当にPairDropを終了しますか",
"message-received": "{{name}}から受信したメッセージ(クリックしてコピー)",
"rate-limit-join-key": "レート制限に到達しました。10秒待ってから再度お試しください。",
"connecting": "接続中…",
"pairing-key-invalidated": "コード{{key}}が失効しました。",
"pairing-key-invalid": "無効なコード",
"connected": "接続しました。",
"pairing-not-persistent": "ペア設定されたデバイスは永続化されていません。",
"text-content-incorrect": "テキストの内容が不正です。",
"message-transfer-completed": "メッセージの送信が完了しました。",
"file-transfer-completed": "ファイルの転送が完了しました。",
"file-content-incorrect": "ファイルの内容が不正です。",
"files-incorrect": "ファイルが間違っています。",
"selected-peer-left": "選択した相手が退出しました。",
"link-received": "{{name}}から受信したリンク(クリックして開く)",
"online": "オンラインに復帰しました",
"public-room-left": "パブリックルーム{{publicRoomId}}から退出しました",
"copied-text": "テキストをクリップボードにコピーしました",
"display-name-random-again": "表示名がもう一度ランダムに生成されました。",
"display-name-changed-permanently": "永続的な表示名が変更されました。",
"copied-to-clipboard-error": "コピーできませんでした。手動でコピーしてください。",
"pairing-success": "デバイスがペア設定されました。",
"clipboard-content-incorrect": "クリップボードの内容が不正です。",
"display-name-changed-temporarily": "このセッションでの表示名が変更されました。",
"copied-to-clipboard": "クリップボードにコピーしました",
"offline": "オフラインです",
"pairing-tabs-error": "同じWebブラウザーの2つのタブをペア設定することはできません。",
"public-room-id-invalid": "無効なルームID",
"click-to-download": "クリックしてダウンロード",
"pairing-cleared": "全てのデバイスのペア設定を解除しました。",
"notifications-enabled": "通知が有効です。",
"online-requirement-pairing": "デバイスをペア設定するにはオンラインである必要があります。",
"ios-memory-limit": "iOSへのファイル送信は一度に200MBまでしかできません",
"online-requirement-public-room": "パブリックルームを作成するにはオンラインである必要があります。",
"copied-text-error": "クリップボードにコピーできませんでした。手動でコピーしてください。",
"download-successful": "{{descriptor}}をダウンロードしました",
"click-to-show": "クリックして表示",
"notifications-permissions-error": "ユーザーが権限のプロンプトを何度か閉じたため、通知の権限がブロックされました。これは、URL バーの横にある鍵アイコンをクリックしてアクセスできるページ情報からリセットできます。",
"pair-url-copied-to-clipboard": "このデバイスをペア設定するリンクをクリップボードにコピーしました",
"room-url-copied-to-clipboard": "パブリックルームへのリンクをクリップボードにコピーしました"
},
"header": {
"cancel-paste-mode": "完了",
"theme-auto_title": "テーマをシステムの設定に自動的に合わせる",
"install_title": "PairDropをインストール",
"theme-dark_title": "常にダークテーマを使用する",
"pair-device_title": "あなたのデバイスを永続的にペア設定する",
"join-public-room_title": "パブリックルームに一時的に参加する",
"notification_title": "通知を有効にする",
"edit-paired-devices_title": "ペア設定したデバイスを編集する",
"language-selector_title": "言語を設定",
"about_title": "PairDropについて",
"about_aria-label": "PairDropについてを開く",
"theme-light_title": "常にライトテーマを使用する"
},
"instructions": {
"x-instructions_mobile": "タップしてファイルを送信または長押ししてメッセージを送信します",
"click-to-send": "クリックして送信",
"activate-paste-mode-and-other-files": "とその他{{count}}個のファイル",
"tap-to-send": "タップして送信",
"activate-paste-mode-base": "他のデバイスでPairDropを開いて送信します",
"no-peers-subtitle": "デバイスをペア設定するかパブリックルームに参加すると、他のネットワーク上からも見つけられるようになります",
"activate-paste-mode-shared-text": "共有されたテキスト",
"x-instructions_desktop": "左クリックしてファイルを送信または右クリックしてメッセージを送信します",
"no-peers-title": "他のデバイスでPairDropを開いてファイルを送信します",
"x-instructions_data-drop-peer": "離すとこの相手に送信します",
"x-instructions_data-drop-bg": "送信したい相手の上で離してください",
"no-peers_data-drop-bg": "送信したい相手の上で離してください"
},
"peer-ui": {
"processing": "処理中…",
"click-to-send-paste-mode": "クリックして{{descriptor}}を送信",
"click-to-send": "クリックしてファイルを送信または右クリックしてメッセージを送信します",
"waiting": "待機中…",
"connection-hash": "エンドツーエンド暗号化のセキュリティを確認するには、両方のデバイスのセキュリティナンバーを確認します",
"preparing": "準備中…",
"transferring": "転送中…"
},
"dialogs": {
"base64-paste-to-send": "ここをタップして{{type}}を送信",
"auto-accept-instructions-2": "そのデバイスから送信される全てのファイルを自動的に承諾します。",
"receive-text-title": "メッセージを受信しました",
"edit-paired-devices-title": "ペア設定したデバイスを編集",
"cancel": "キャンセル",
"auto-accept-instructions-1": "有効化",
"pair-devices-title": "デバイスを永続的にペア設定",
"download": "ダウンロード",
"title-file": "ファイル",
"base64-processing": "処理中…",
"decline": "拒否",
"receive-title": "{{descriptor}}を受信しました",
"leave": "退出",
"join": "参加",
"title-image-plural": "複数の画像",
"send": "送信",
"base64-tap-to-paste": "ここをタップして{{type}}を貼り付け",
"base64-text": "テキスト",
"copy": "コピー",
"file-other-description-image": "と1個の他の画像",
"temporary-public-room-title": "一時的なパブリックルーム",
"base64-files": "ファイル",
"has-sent": "が送信しました:",
"file-other-description-file": "と1個の他のファイル",
"close": "閉じる",
"system-language": "システムの言語",
"unpair": "ペア解除",
"title-image": "画像",
"file-other-description-file-plural": "と{{count}}個の他のファイル",
"would-like-to-share": "が以下のファイルを共有しようとしています",
"send-message-to": "このデバイスにメッセージを送信:",
"language-selector-title": "言語を設定",
"pair": "ペア設定",
"hr-or": "または",
"scan-qr-code": "もしくはQRコードをスキャンします。",
"input-key-on-this-device": "このキーを他のデバイスに入力する",
"download-again": "もう一度ダウンロードする",
"accept": "承諾",
"paired-devices-wrapper_data-empty": "ペア設定したデバイスはありません。",
"enter-key-from-another-device": "他のデバイスに表示されたキーをここに入力します。",
"share": "共有",
"auto-accept": "自動承諾",
"title-file-plural": "複数のファイル",
"send-message-title": "メッセージを送信",
"input-room-id-on-another-device": "このルームIDを他のデバイスに入力",
"file-other-description-image-plural": "と{{count}}個の他の画像",
"enter-room-id-from-another-device": "他のデバイスに表示された参加したいルームのIDを入力します。",
"message_title": "送信するメッセージを挿入",
"pair-devices-qr-code_title": "クリックしてこのデバイスをペア設定するリンクをコピー",
"public-room-qr-code_title": "クリックしてパブリックルームへのリンクをコピー"
},
"about": {
"claim": "デバイス間でファイルを転送する最も簡単な方法",
"tweet_title": "PairDropについてツイートする",
"close-about_aria-label": "PairDropについてを閉じる",
"buy-me-a-coffee_title": "コーヒーをおごってください!",
"github_title": "GitHubでPairDropを見る",
"faq_title": "FAQ"
},
"document-titles": {
"file-transfer-requested": "ファイルの転送がリクエストされました",
"image-transfer-requested": "画像の転送がリクエストされました",
"message-received-plural": "{{count}}個のメッセージを受信しました",
"message-received": "メッセージを受信しました",
"file-received": "ファイルを受信しました",
"file-received-plural": "{{count}}個のファイルを受信しました"
}
}

138
public/lang/nb.json Normal file
View File

@@ -0,0 +1,138 @@
{
"header": {
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
"about_title": "Om PairDrop",
"about_aria-label": "Åpne «Om PairDrop»",
"theme-auto_title": "Juster drakt til system",
"theme-light_title": "Alltid bruk lys drakt",
"theme-dark_title": "Alltid bruk mørk drakt",
"notification_title": "Skru på merknader",
"cancel-paste-mode": "Ferdig",
"install_title": "Installer PairDrop",
"pair-device_title": "Sammenkoble enhet"
},
"footer": {
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
"display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
"known-as": "Du er kjent som:",
"paired-devices": "sammenkoblede enheter",
"routed": "Sendes gjennom tjeneren"
},
"instructions": {
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
"click-to-send": "Klikk for å sende",
"no-peers_data-drop-bg": "Slipp for å velge mottager",
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
"tap-to-send": "Trykk for å sende",
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
"activate-paste-mode-shared-text": "delt tekst"
},
"dialogs": {
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
"pair-devices-title": "Sammenkoble enheter",
"would-like-to-share": "ønsker å dele",
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter.",
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
"accept": "Godta",
"has-sent": "har sendt:",
"base64-paste-to-send": "Trykk her for å sende {{type}}",
"base64-text": "tekst",
"base64-files": "filer",
"file-other-description-image-plural": "og {{count}} andre bilder",
"receive-title": "{{descriptor}} mottatt",
"send-message-title": "Send melding",
"base64-processing": "Behandler…",
"close": "Lukk",
"decline": "Avslå",
"download": "Last ned",
"copy": "Kopier",
"pair": "Sammenkoble",
"cancel": "Avbryt",
"scan-qr-code": "eller skann QR-koden.",
"auto-accept-instructions-1": "Aktiver",
"receive-text-title": "Melding mottatt",
"auto-accept": "auto-godkjenn",
"share": "Del",
"send-message-to": "Send en melding til",
"send": "Send",
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
"file-other-description-image": "og ett annet bilde",
"file-other-description-file-plural": "og {{count}} andre filer",
"title-file-plural": "Filer",
"download-again": "Last ned igjen",
"file-other-description-file": "og én annen fil",
"title-image": "Bilde",
"title-file": "Fil",
"title-image-plural": "Bilder"
},
"about": {
"close-about_aria-label": "Lukk «Om PairDrop»",
"faq_title": "Ofte stilte spørsmål",
"claim": "Den enkleste måten å overføre filer mellom enheter",
"buy-me-a-coffee_title": "Spander drikke.",
"tweet_title": "Tvitre om PairDrop",
"github_title": "PairDrop på GitHub"
},
"notifications": {
"copied-to-clipboard": "Kopiert til utklippstavlen",
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
"notifications-enabled": "Merknader påskrudd.",
"click-to-show": "Klikk for å vise",
"copied-text": "Tekst kopiert til utklippstavlen",
"connected": "Tilkoblet.",
"online": "Du er tilbake på nett",
"file-transfer-completed": "Filoverføring utført.",
"selected-peer-left": "Valgt likemann dro.",
"pairing-key-invalid": "Ugyldig nøkkel",
"connecting": "Kobler til …",
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
"offline": "Du er frakoblet",
"online-requirement": "Du må være på nett for å koble sammen enheter.",
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
"text-content-incorrect": "Tekstinnholdet er uriktig.",
"file-content-incorrect": "Filinnholdet er uriktig.",
"click-to-download": "Klikk for å laste ned",
"message-transfer-completed": "Meldingsoverføring utført.",
"download-successful": "{{descriptor}} nedlastet",
"pairing-success": "Enheter sammenkoblet.",
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne",
"files-incorrect": "Filene er uriktige.",
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
},
"document-titles": {
"file-received": "Fil mottatt",
"file-received-plural": "{{count}} filer mottatt",
"message-received": "Melding mottatt",
"file-transfer-requested": "Filoverføring forespurt",
"message-received-plural": "{{count}} meldinger mottatt"
},
"peer-ui": {
"preparing": "Forbereder …",
"waiting": "Venter…",
"processing": "Behandler …",
"transferring": "Overfører …",
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
}
}

159
public/lang/nl.json Normal file
View File

@@ -0,0 +1,159 @@
{
"footer": {
"webrtc": "als WebRTC niet beschikbaar is.",
"public-room-devices_title": "U kan door apparaten gevonden worden in deze openbare ruimte, ongeacht van het netwerk.",
"display-name_data-placeholder": "Laden…",
"display-name_title": "Bewerk uw apparaatnaam permanent",
"traffic": "Dataverkeer is",
"paired-devices_title": "U kan gevonden worden door gekoppelde apparaten, ongeacht van het netwerk.",
"public-room-devices": "in kamer {{roomId}}",
"paired-devices": "door gekoppelde apparaten",
"on-this-network": "op dit netwerk",
"routed": "door de server geleid",
"discovery": "U bent zichtbaar:",
"on-this-network_title": "U kan door iedereen gevonden worden op dit netwerk.",
"known-as": "U staat bekend als:"
},
"notifications": {
"request-title": "{{name}} zou graag {{count}}{{descriptor}} overdragen",
"unfinished-transfers-warning": "Nog niet alle overdrachten zijn compleet. Weet u zeker dat u PairDrop sluiten?",
"message-received": "Bericht ontvangen van {{name}} - Klik om te kopiëren",
"rate-limit-join-key": "Tempolimiet bereikt. Wacht 10 seconde en probeer opnieuw.",
"connecting": "Verbinden…",
"pairing-key-invalidated": "Sleutel {{key}} ongeldig.",
"pairing-key-invalid": "Ongeldige sleutel",
"connected": "Verbonden.",
"pairing-not-persistent": "Gekoppelde apparaten zijn niet persistent.",
"text-content-incorrect": "Tekst inhoud is incorrect.",
"message-transfer-completed": "Berichtsoverdracht compleet.",
"file-transfer-completed": "Bestandsoverdracht compleet.",
"file-content-incorrect": "Bestandsinhoud is incorrect.",
"files-incorrect": "Bestanden zijn incorrect.",
"selected-peer-left": "Gekozen peer is vertrokken.",
"link-received": "Link van {{name}} ontvangen - Klik om te openen",
"online": "U bent terug online",
"public-room-left": "Openbare ruimte {{publicRoomId}} verlaten",
"copied-text": "Tekst naar klembord gekopieërd",
"display-name-random-again": "De weergavenaam is opnieuw willekeurig gegenereerd.",
"display-name-changed-permanently": "De weergavenaam is permanent gewijzigd.",
"copied-to-clipboard-error": "Kopiëren is niet mogelijk. Kopieer handmatig.",
"pairing-success": "Apparaten gekoppeld.",
"clipboard-content-incorrect": "De inhoud van het klembord is incorrect.",
"display-name-changed-temporarily": "De weergavenaam is alleen voor deze sessie gewijzigd.",
"copied-to-clipboard": "Gekopieerd naar klembord",
"offline": "U bent offline",
"pairing-tabs-error": "Twee webbrowser tabbladen koppelen in is onmogelijk.",
"public-room-id-invalid": "Ongeldig kamer ID",
"click-to-download": "Klik om te downloaden",
"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",
"click-to-show": "Klik om te tonen"
},
"header": {
"cancel-paste-mode": "Klaar",
"theme-auto_title": "Gebruik systeemstijl",
"install_title": "PairDrop installeren",
"theme-dark_title": "Altijd donkere modus gebruiken",
"pair-device_title": "Koppel uw apparaten permanent",
"join-public-room_title": "Openbare ruimte tijdelijk betreden",
"notification_title": "Meldingen inschakelen",
"edit-paired-devices_title": "Gekoppelde apparaten bewerken",
"language-selector_title": "Taal Selecteren",
"about_title": "Over PairDrop",
"about_aria-label": "Open Over PairDrop",
"theme-light_title": "Altijd lichte modus gebruiken"
},
"instructions": {
"x-instructions_mobile": "Tik om bestanden te versturen of houdt vast om een bericht te sturen",
"click-to-send": "Klik om te verzenden",
"activate-paste-mode-and-other-files": "en {{count}} andere bestanden",
"tap-to-send": "Tik om te verzenden",
"activate-paste-mode-base": "Open PairDrop op andere apparaten om te verzenden",
"no-peers-subtitle": "Koppel apparaten of betreed een openbare ruimte om op andere netwerken zichtbaar te worden",
"activate-paste-mode-shared-text": "gedeelde tekst",
"x-instructions_desktop": "Klik om bestanden te versturen of rechtsklik om een bericht te sturen",
"no-peers-title": "Open PairDrop op andere apparaten om bestanden te versturen",
"x-instructions_data-drop-peer": "Laat los om naar peer te versturen",
"x-instructions_data-drop-bg": "Loslaten om ontvanger te selecteren",
"no-peers_data-drop-bg": "Loslaten om ontvanger te kiezen"
},
"peer-ui": {
"processing": "Verwerken…",
"click-to-send-paste-mode": "Klik om {{descriptor}} te versturen",
"click-to-send": "Klik om bestanden te versturen of rechtsklik om een bericht te versturen",
"waiting": "Wachten…",
"connection-hash": "Vergelijk dit veiligheidsnummer op beide apparaten, om de beveiliging van de eind-tot-eind versleuteling te verifiëren",
"preparing": "Voorbereiden…",
"transferring": "Overdragen…"
},
"dialogs": {
"base64-paste-to-send": "Plak hier om {{type}} te versturen",
"auto-accept-instructions-2": "om automatisch alle bestanden van dat apparaat te accepteren.",
"receive-text-title": "Bericht Ontvangen",
"edit-paired-devices-title": "Gekoppelde Apparaten Bewerken",
"cancel": "Annuleer",
"auto-accept-instructions-1": "Activeer",
"pair-devices-title": "Koppel Apparaten Permanent",
"download": "Download",
"title-file": "Bestand",
"base64-processing": "Verwerken…",
"decline": "Afwijzen",
"receive-title": "{{descriptor}} Ontvangen",
"leave": "Verlaten",
"join": "Betreden",
"title-image-plural": "Afbeeldingen",
"send": "Verzenden",
"base64-tap-to-paste": "Hier tikken om {{type}} te plakken",
"base64-text": "tekst",
"copy": "Kopiëren",
"file-other-description-image": "en één andere afbeelding",
"temporary-public-room-title": "Tijdelijke Openbare Ruimte",
"base64-files": "bestanden",
"has-sent": "stuurde het volgende:",
"file-other-description-file": "en één ander bestand",
"close": "Sluiten",
"system-language": "Systeemtaal",
"unpair": "Ontkoppel",
"title-image": "Afbeelding",
"file-other-description-file-plural": "en {{count}} andere bestanden",
"would-like-to-share": "zou graag het volgende willen delen",
"send-message-to": "Verstuur een bericht naar",
"language-selector-title": "Taal Instellen",
"pair": "Koppel",
"hr-or": "OF",
"scan-qr-code": "of scan de QR-code.",
"input-key-on-this-device": "Voer deze sleutel in op een ander apparaat",
"download-again": "Download opnieuw",
"accept": "Accepteren",
"paired-devices-wrapper_data-empty": "Geen gekoppelde apparaten.",
"enter-key-from-another-device": "Voer hier de sleutel van een ander apparaat in.",
"share": "Delen",
"auto-accept": "automatisch-accepteren",
"title-file-plural": "Bestanden",
"send-message-title": "Bericht Sturen",
"input-room-id-on-another-device": "Voer de kamer ID in op een ander apparaat",
"file-other-description-image-plural": "en {{count}} andere afbeeldingen",
"enter-room-id-from-another-device": "Voer de kamer ID van een ander apparaat hier in."
},
"about": {
"claim": "De makkelijkste manier om bestanden tussen apparaten te versturen",
"tweet_title": "Tweet over PairDrop",
"close-about_aria-label": "Sluit Over PairDrop",
"buy-me-a-coffee_title": "Koop een kopje koffie voor mij!",
"github_title": "PairDrop op GitHub",
"faq_title": "Veel gestelde vragen"
},
"document-titles": {
"file-transfer-requested": "Bestandsoverdracht verzocht",
"image-transfer-requested": "Afbeeldingsoverdracht verzocht",
"message-received-plural": "{{count}} berichten ontvangen",
"message-received": "Bericht ontvangen",
"file-received": "Bestand ontvangen",
"file-received-plural": "{{count}} bestanden ontvangen"
}
}

165
public/lang/ro.json Normal file
View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "dacă WebRTC nu este disponibil.",
"public-room-devices_title": "Poți fi descoperit de dispozitivele din această cameră publică, independent de rețea.",
"display-name_data-placeholder": "Se încarcă…",
"display-name_title": "Editați permanent numele dispozitivului tău",
"traffic": "Traficul este",
"paired-devices_title": "Poți fi descoperit în orice moment de dispozitivele cuplate, indiferent de rețea.",
"public-room-devices": "în camera {{roomId}}",
"paired-devices": "prin dispozitive împerecheate",
"on-this-network": "în această rețea",
"routed": "rutate prin server",
"discovery": "Poți fi descoperit:",
"on-this-network_title": "Poți fi descoperit de toată lumea din această rețea.",
"known-as": "Ești cunoscut ca:"
},
"notifications": {
"request-title": "{{name}} ar dori să transfere {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Există transferuri neterminate. Sigur vrei să închizi PairDrop?",
"message-received": "Mesaj primit de {{name}} - Apasă pentru a copia",
"rate-limit-join-key": "A fost atinsă limita ratei. Așteptați 10 secunde și încercați din nou.",
"connecting": "Conectarea…",
"pairing-key-invalidated": "Cheia {{key}} invalidată.",
"pairing-key-invalid": "Cheie invalidă",
"connected": "Conectat.",
"pairing-not-persistent": "Dispozitivele cuplate nu sunt persistente.",
"text-content-incorrect": "Conținutul textului este incorect.",
"message-transfer-completed": "Transferul mesajului este finalizat.",
"file-transfer-completed": "Transfer de fișiere finalizat.",
"file-content-incorrect": "Conținutul fișierului este incorect.",
"files-incorrect": "Fișierele sunt incorecte.",
"selected-peer-left": "Selectat peer a plecat.",
"link-received": "Link primit de {{name}} - Apasă pentru a deschide",
"online": "Ați revenit online",
"public-room-left": "Plecat din camera publică {{publicRoomId}}",
"copied-text": "Text copiat în clipboard",
"display-name-random-again": "Numele afișat este din nou generat aleatoriu.",
"display-name-changed-permanently": "Numele afișat este schimbat permanent.",
"copied-to-clipboard-error": "Copierea nu este posibilă. Copiați manual.",
"pairing-success": "Dispozitive asociate.",
"clipboard-content-incorrect": "Conținutul clipboard-ului este incorect.",
"display-name-changed-temporarily": "Numele afișat se modifică numai pentru această sesiune.",
"copied-to-clipboard": "Copiat în clipboard",
"offline": "Ești offline",
"pairing-tabs-error": "Cuplarea între două file de browser web este imposibilă.",
"public-room-id-invalid": "ID-ul camerei invalid",
"click-to-download": "Apasă pentru a descărca",
"pairing-cleared": "Toate dispozitivele sunt decuplate.",
"notifications-enabled": "Notificări activate.",
"online-requirement-pairing": "Trebuie să fiți online pentru a asocia dispozitivele.",
"ios-memory-limit": "Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan",
"online-requirement-public-room": "Trebuie să fiți online pentru a crea o cameră publică.",
"copied-text-error": "Scrierea în clipboard a eșuat. Copiați manual!",
"download-successful": "{{descriptor}} descărcat",
"click-to-show": "Apasă pentru a arăta",
"notifications-permissions-error": "Permisiunea de notificare a fost blocată deoarece utilizatorul a respins de mai multe ori solicitarea de permisiune. Acest lucru poate fi resetat în Info pagină, care poate fi accesat făcând clic pe pictograma de blocare de lângă bara URL.",
"pair-url-copied-to-clipboard": "Link pentru a asocia acest dispozitiv copiat în clipboard",
"room-url-copied-to-clipboard": "Link către sala publică copiat în clipboard"
},
"header": {
"cancel-paste-mode": "Gata",
"theme-auto_title": "Adaptează tema la sistem",
"install_title": "Instalează PairDrop",
"theme-dark_title": "Utilizați mereu tema întunecoasă",
"pair-device_title": "Împerechează-ți permanent dispozitivele",
"join-public-room_title": "Alătură-te temporar camerei publice",
"notification_title": "Activați notificări",
"edit-paired-devices_title": "Editați dispozitivele împerecheate",
"language-selector_title": "Setează Limba",
"about_title": "Despre PairDrop",
"about_aria-label": "Deschide Despre PairDrop",
"theme-light_title": "Utilizați mereu tema luminoasă"
},
"instructions": {
"x-instructions_mobile": "Atingeți pentru a trimite fișiere sau atingeți lung pentru a trimite un mesaj",
"click-to-send": "Clic pentru a trimite",
"activate-paste-mode-and-other-files": "și {{count}} alte fișiere",
"tap-to-send": "Atinge pentru a trimite",
"activate-paste-mode-base": "Deschideți PairDrop pe alte dispozitive pentru a trimite",
"no-peers-subtitle": "Împerecheați dispozitive sau intrați într-o cameră publică pentru a fi descoperit în alte rețele",
"activate-paste-mode-shared-text": "text partajat",
"x-instructions_desktop": "Dați clic pentru a trimite fișiere sau dați clic dreapta pentru a trimite un mesaj",
"no-peers-title": "Deschideți PairDrop pe alte dispozitive pentru a trimite fișiere",
"x-instructions_data-drop-peer": "Eliberare pentru a trimite la peer",
"x-instructions_data-drop-bg": "Eliberați pentru a selecta recipientul",
"no-peers_data-drop-bg": "Eliberare pentru a selecta recipientul"
},
"peer-ui": {
"processing": "Procesarea…",
"click-to-send-paste-mode": "Apasă pentru a trimite {{descriptor}}",
"click-to-send": "Apasă pentru a trimite fișiere sau apasă cu butonul din dreapta pentru a trimite un mesaj",
"waiting": "Așteptând…",
"connection-hash": "Pentru a verifica securitatea criptării end-to-end, comparați acest număr de securitate pe ambele dispozitive",
"preparing": "Pregătirea…",
"transferring": "Transferul…"
},
"dialogs": {
"base64-paste-to-send": "Lipiți aici pentru a trimite {{type}}",
"auto-accept-instructions-2": "pentru a accepta automat toate fișierele trimise de la dispozitivul respectiv.",
"receive-text-title": "Mesaj primit",
"edit-paired-devices-title": "Editați dispozitivele asociate",
"cancel": "Anulează",
"auto-accept-instructions-1": "Activează",
"pair-devices-title": "Împerecherea permanentă a dispozitivelor",
"download": "Descarcă",
"title-file": "Fişier",
"base64-processing": "Procesarea…",
"decline": "Declin",
"receive-title": "{{descriptor}} Primit",
"leave": "Pleacă",
"join": "Alătură-te",
"title-image-plural": "Imagini",
"send": "Trimite",
"base64-tap-to-paste": "Atinge aici pentru a lipi {{type}}",
"base64-text": "text",
"copy": "Copiază",
"file-other-description-image": "și 1 altă imagine",
"temporary-public-room-title": "Cameră publică temporară",
"base64-files": "fişiere",
"has-sent": "a trimis:",
"file-other-description-file": "și 1 alt fișier",
"close": "Închide",
"system-language": "Limba Sistemului",
"unpair": "Decuplează",
"title-image": "Imagine",
"file-other-description-file-plural": "și {{count}} alte fișiere",
"would-like-to-share": "ar dori să împărtășească",
"send-message-to": "Trimite un mesaj la",
"language-selector-title": "Setați Limba",
"pair": "Cuplu",
"hr-or": "SAU",
"scan-qr-code": "sau scanați codul QR.",
"input-key-on-this-device": "Introduceți această cheie pe un alt dispozitiv",
"download-again": "Descarcă din nou",
"accept": "Acceptă",
"paired-devices-wrapper_data-empty": "Nu sunt dispozitive asociate.",
"enter-key-from-another-device": "Introduceți aici cheia de la un alt dispozitiv.",
"share": "Partajați",
"auto-accept": "auto-acceptare",
"title-file-plural": "Fişiere",
"send-message-title": "Trimite un mesaj",
"input-room-id-on-another-device": "Introduceți acest ID de cameră pe un alt dispozitiv",
"file-other-description-image-plural": "și {{count}} alte imagini",
"enter-room-id-from-another-device": "Introdu ID-ul camerei de pe un alt dispozitiv pentru a intra în cameră.",
"message_title": "Inserați mesajul de trimis",
"pair-devices-qr-code_title": "Dați clic pentru a copia link-ul pentru a asocia acest dispozitiv",
"public-room-qr-code_title": "Dați clic pentru a copia link-ul în sala publică"
},
"about": {
"claim": "Cel mai simplu mod de a transfera fișiere între dispozitive",
"tweet_title": "Tweet despre PairDrop",
"close-about_aria-label": "Închide Despre PairDrop",
"buy-me-a-coffee_title": "Cumpără-mi o cafea!",
"github_title": "PairDrop pe GitHub",
"faq_title": "Întrebări frecvente"
},
"document-titles": {
"file-transfer-requested": "Transfer de fișiere cerut",
"message-received-plural": "{{count}}} Mesaje primite",
"message-received": "Mesaj primit",
"file-received": "Fișier Primit",
"file-received-plural": "{{count}} Fișiere Primite",
"image-transfer-requested": "Transfer de imagine solicitat"
}
}

167
public/lang/ru.json Normal file
View File

@@ -0,0 +1,167 @@
{
"header": {
"about_aria-label": "Открыть страницу \"О сервисе\"",
"pair-device_title": "Связать ваши устройства навсегда",
"install_title": "Установить PairDrop",
"cancel-paste-mode": "Выполнено",
"edit-paired-devices_title": "Редактировать связанные устройства",
"notification_title": "Включить уведомления",
"about_title": "О сервисе",
"theme-auto_title": "Адаптировать тему к системной автоматически",
"theme-dark_title": "Всегда использовать темную тему",
"theme-light_title": "Всегда использовать светлую тему",
"join-public-room_title": "Войти на время в публичную комнату",
"language-selector_title": "Выбрать язык"
},
"instructions": {
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
"click-to-send": "Нажмите, чтобы отправить",
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
"tap-to-send": "Прикоснитесь, чтобы отправить",
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
"no-peers-subtitle": "Свяжите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
"activate-paste-mode-shared-text": "общий текст"
},
"footer": {
"display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик",
"paired-devices": "связанными устройствами",
"known-as": "Вы известны под именем:",
"on-this-network": "в этой сети",
"display-name_title": "Изменить имя вашего устройства навсегда",
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
"paired-devices_title": "Вы можете быть обнаружены связанными устройствами в любое время вне зависимости от сети.",
"public-room-devices": "в комнате {{roomId}}",
"discovery": "Вы можете быть обнаружены:",
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
},
"dialogs": {
"edit-paired-devices-title": "Редактировать Связанные Устройства",
"auto-accept": "автоприем",
"close": "Закрыть",
"decline": "Отклонить",
"share": "Поделиться",
"would-like-to-share": "хотел бы поделиться",
"has-sent": "отправил:",
"paired-devices-wrapper_data-empty": "Нет связанных устройств.",
"download": "Скачать",
"receive-text-title": "Сообщение получено",
"send": "Отправить",
"send-message-to": "Отправить сообщение",
"send-message-title": "Отправить сообщение",
"copy": "Копировать",
"base64-files": "файлы",
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
"base64-processing": "Обработка…",
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
"base64-text": "текст",
"title-file": "Файл",
"title-file-plural": "Файлы",
"title-image": "Изображение",
"title-image-plural": "Изображения",
"download-again": "Скачать еще раз",
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
"pair-devices-title": "Соединить устройства навсегда",
"input-key-on-this-device": "На другом устройстве введите этот ключ",
"scan-qr-code": "или отсканируйте QR-код.",
"cancel": "Отменить",
"pair": "Подключить",
"accept": "Принять",
"auto-accept-instructions-1": "Активировать",
"file-other-description-file": "и 1 другой файл",
"file-other-description-image-plural": "и {{count}} других изображений",
"file-other-description-image": "и 1 другое изображение",
"file-other-description-file-plural": "и {{count}} других файлов",
"receive-title": "{{descriptor}} получен",
"system-language": "Язык системы",
"unpair": "Отвязать",
"language-selector-title": "Установить язык",
"hr-or": "ИЛИ",
"input-room-id-on-another-device": "На другом устройстве введите этот ID комнаты",
"leave": "Покинуть",
"join": "Войти",
"enter-room-id-from-another-device": "Введите ID комнаты с другого устройства, чтобы войти в нее.",
"temporary-public-room-title": "Временная публичная комната",
"message_title": "Вставьте сообщение для отправки",
"pair-devices-qr-code_title": "Нажмите, чтобы скопировать ссылку для привязки этого устройства",
"public-room-qr-code_title": "Нажмите, чтобы скопировать ссылку на публичную комнату"
},
"about": {
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
"claim": "Самый простой способ передачи файлов между устройствами",
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
"buy-me-a-coffee_title": "Купить мне кофе!",
"github_title": "PairDrop на GitHub",
"tweet_title": "Твит о PairDrop",
"faq_title": "Часто задаваемые вопросы"
},
"notifications": {
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
"pairing-success": "Устройства связаны.",
"pairing-tabs-error": "Связка двух вкладок браузера невозможна.",
"copied-to-clipboard": "Скопировано в буфер обмена",
"pairing-not-persistent": "Связанные устройства непостоянны.",
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
"notifications-enabled": "Уведомления включены.",
"text-content-incorrect": "Содержание текста неверно.",
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
"connected": "Подключено.",
"copied-text": "Текст скопирован в буфер обмена",
"online": "Вы снова в сети",
"offline": "Вы находитесь вне сети",
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
"files-incorrect": "Файлы неверны.",
"message-transfer-completed": "Передача сообщения завершена.",
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
"selected-peer-left": "Выбранный узел вышел.",
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
"pairing-cleared": "Все устройства отвязаны.",
"pairing-key-invalid": "Неверный ключ",
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
"click-to-download": "Нажмите, чтобы скачать",
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
"click-to-show": "Нажмите, чтобы показать",
"connecting": "Подключение…",
"download-successful": "{{descriptor}} загружен",
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
"file-content-incorrect": "Содержимое файла неверно.",
"file-transfer-completed": "Передача файла завершена.",
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
"public-room-id-invalid": "Неверный ID комнаты",
"online-requirement-pairing": "Для связки устройств необходимо находиться быть онлайн.",
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн.",
"notifications-permissions-error": "Уведомления были заблокированы, так как пользователь отклонил запрос на их работу несколько раз. Это можно изменить в меню \"О странице\", которое может быть вызвано нажатием на иконку замочка рядом со строкой адреса сайта.",
"pair-url-copied-to-clipboard": "Ссылка для привязки этого устройства была скопирована в буфер обмена",
"room-url-copied-to-clipboard": "Ссылка на публичную комнату была скопирована в буфер обмена"
},
"peer-ui": {
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
"preparing": "Подготовка…",
"transferring": "Передача…",
"processing": "Обработка…",
"waiting": "Ожидание…",
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
},
"document-titles": {
"file-received-plural": "{{count}} файлов получено",
"message-received-plural": "{{count}} сообщений получено",
"file-received": "Файл получен",
"file-transfer-requested": "Запрошена передача файлов",
"message-received": "Сообщение получено",
"image-transfer-requested": "Запрошена передача изображений"
}
}

25
public/lang/tr.json Normal file
View File

@@ -0,0 +1,25 @@
{
"header": {
"about_title": "PairDrop Hakkında",
"about_aria-label": "PairDrop Hakkında Aç",
"theme-auto_title": "Temayı Sisteme Uyarla",
"theme-light_title": "Daima Açık Tema Kullan",
"theme-dark_title": "Daima Koyu Tema Kullan",
"notification_title": "Bildirimleri Etkinleştir",
"install_title": "PairDrop'u Yükle",
"pair-device_title": "Cihaz Eşleştir",
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
"cancel-paste-mode": "Bitti"
},
"instructions": {
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
},
"footer": {
"display-name_data-placeholder": "Yükleniyor…",
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
},
"dialogs": {
"cancel": "İptal",
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
}
}

166
public/lang/zh-CN.json Normal file
View File

@@ -0,0 +1,166 @@
{
"header": {
"about_title": "关于 PairDrop",
"about_aria-label": "打开 关于 PairDrop",
"theme-light_title": "总是使用明亮主题",
"install_title": "安装 PairDrop",
"pair-device_title": "永久配对您的设备",
"theme-auto_title": "主题适应系统",
"theme-dark_title": "总是使用暗黑主题",
"notification_title": "开启通知",
"edit-paired-devices_title": "管理已配对设备",
"cancel-paste-mode": "完成",
"join-public-room_title": "暂时加入公共房间",
"language-selector_title": "设置语言"
},
"instructions": {
"x-instructions_data-drop-peer": "释放以发送到此设备",
"no-peers_data-drop-bg": "释放来选择接收者",
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 以便在其他网络上可见",
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
"x-instructions_data-drop-bg": "释放来选择接收者",
"click-to-send": "点击发送",
"tap-to-send": "轻触发送",
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
"activate-paste-mode-shared-text": "分享文本"
},
"footer": {
"routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:",
"display-name_data-placeholder": "加载中…",
"display-name_title": "修改你的默认设备名",
"on-this-network": "在此网络上",
"paired-devices": "已配对的设备",
"traffic": "流量将",
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
"public-room-devices": "在房间 {{roomId}} 中",
"discovery": "您可以被发现:",
"on-this-network_title": "您可以被这个网络上的每个人发现。"
},
"dialogs": {
"pair-devices-title": "配对新设备(常驻)",
"input-key-on-this-device": "在另一个设备上输入这串数字",
"base64-text": "信息",
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
"edit-paired-devices-title": "管理已配对的设备",
"pair": "配对",
"cancel": "取消",
"scan-qr-code": "或者 扫描二维码。",
"paired-devices-wrapper_data-empty": "无已配对设备。",
"auto-accept-instructions-1": "启用",
"auto-accept": "自动接收",
"decline": "拒绝",
"base64-processing": "处理中…",
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
"would-like-to-share": "想要分享",
"accept": "接收",
"close": "关闭",
"share": "分享",
"download": "保存",
"send": "发送",
"receive-text-title": "收到信息",
"copy": "复制",
"send-message-title": "发送信息",
"send-message-to": "发了一条信息给",
"has-sent": "发送了:",
"base64-files": "文件",
"file-other-description-file": "和 1 个其他的文件",
"file-other-description-image": "和 1 个其他的图片",
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
"title-image-plural": "图片",
"receive-title": "收到 {{descriptor}}",
"title-image": "图片",
"title-file": "文件",
"title-file-plural": "文件",
"download-again": "再次保存",
"system-language": "跟随系统语言",
"unpair": "取消配对",
"language-selector-title": "设置语言",
"hr-or": "或者",
"input-room-id-on-another-device": "在另一个设备上输入这串房间号",
"leave": "离开",
"join": "加入",
"temporary-public-room-title": "临时公共房间",
"enter-room-id-from-another-device": "在另一个设备上输入这串房间号来加入房间。",
"message_title": "插入要发送的消息",
"pair-devices-qr-code_title": "单击复制和此设备配对的链接",
"public-room-qr-code_title": "单击复制公共房间链接"
},
"about": {
"faq_title": "常见问题",
"close-about_aria-label": "关闭 关于 PairDrop",
"github_title": "PairDrop 在 GitHub 上开源",
"claim": "最简单的跨设备传输方案",
"buy-me-a-coffee_title": "帮我买杯咖啡!",
"tweet_title": "关于 PairDrop 的推特"
},
"notifications": {
"display-name-changed-permanently": "展示的名字已经永久变更。",
"display-name-changed-temporarily": "展示的名字仅在此会话中变更。",
"display-name-random-again": "展示的名字已再次随机生成。",
"download-successful": "{{descriptor}} 已下载",
"pairing-tabs-error": "无法配对两个浏览器标签页。",
"pairing-success": "新设备已配对。",
"pairing-not-persistent": "配对的设备不是持久的。",
"pairing-key-invalid": "无效配对码",
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
"text-content-incorrect": "文本内容不合法。",
"file-content-incorrect": "文件内容不合法。",
"clipboard-content-incorrect": "剪贴板内容不合法。",
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
"click-to-show": "点击展示",
"copied-text": "复制到剪贴板",
"selected-peer-left": "选择的设备已离开。",
"pairing-cleared": "所有设备已解除配对。",
"copied-to-clipboard": "已复制到剪贴板",
"notifications-enabled": "通知已启用。",
"copied-text-error": "写入剪贴板失败。请手动复制!",
"click-to-download": "点击以保存",
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
"message-transfer-completed": "信息传输已完成。",
"offline": "你未连接到网络",
"online": "你已重新连接到网络",
"connected": "已连接。",
"online-requirement": "你需要连接网络来配对新设备。",
"files-incorrect": "文件不合法。",
"file-transfer-completed": "文件传输已完成。",
"connecting": "连接中…",
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
"public-room-left": "已退出公共房间 {{publicRoomId}}",
"copied-to-clipboard-error": "无法复制。请手动复制。",
"public-room-id-invalid": "无效的房间号",
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。",
"notifications-permissions-error": "因用户数次拒绝了权限授予提示,通知权限已被拦截。可以在“页面信息”中重置它,要访问“页面信息”请单击地址栏旁的挂锁图标。",
"pair-url-copied-to-clipboard": "已将和此设备配对的链接复制到剪贴板",
"room-url-copied-to-clipboard": "已将公共房间的链接复制到剪贴板"
},
"document-titles": {
"message-received": "收到信息",
"message-received-plural": "收到 {{count}} 条信息",
"file-transfer-requested": "文件传输请求",
"file-received-plural": "收到 {{count}} 个文件",
"file-received": "收到文件",
"image-transfer-requested": "图片传输请求"
},
"peer-ui": {
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
"click-to-send": "点击以发送文件 或 右键来发送信息",
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
"preparing": "准备中…",
"waiting": "请等待…",
"transferring": "传输中…",
"processing": "处理中…"
}
}

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,172 @@
class Localization {
constructor() {
Localization.defaultLocale = "en";
Localization.supportedLocales = ["ar", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "ro", "ru", "zh-CN"];
Localization.supportedLocalesRTL = ["ar"];
Localization.translations = {};
Localization.defaultTranslations = {};
Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages);
let storedLanguageCode = localStorage.getItem("language-code");
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
? storedLanguageCode
: Localization.systemLocale;
Localization.setTranslation(Localization.initialLocale)
.then(_ => {
console.log("Initial translation successful.");
Events.fire("initial-translation-loaded");
});
}
static isSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
}
static isRTLLanguage(locale) {
return Localization.supportedLocalesRTL.indexOf(locale) > -1;
}
static getSupportedOrDefault(locales) {
let localesGeneric = locales
.map(locale => locale.split("-")[0])
.filter(locale => locales.indexOf(locale) === -1);
return locales.find(Localization.isSupported)
|| localesGeneric.find(Localization.isSupported)
|| Localization.defaultLocale;
}
static async setTranslation(locale) {
if (!locale) locale = Localization.systemLocale;
await Localization.setLocale(locale)
await Localization.translatePage();
const htmlRootNode = document.querySelector('html');
if (Localization.isRTLLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
} else {
htmlRootNode.removeAttribute('dir');
}
htmlRootNode.setAttribute('lang', locale);
console.log("Page successfully translated",
`System language: ${Localization.systemLocale}`,
`Selected language: ${locale}`
);
Events.fire("translation-loaded");
}
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
if(!newTranslations) return false;
Localization.locale = newLocale;
Localization.translations = newTranslations;
}
static getLocale() {
return Localization.locale;
}
static isSystemLocale() {
return !localStorage.getItem('language-code');
}
static async fetchTranslationsFor(newLocale) {
const response = await fetch(`lang/${newLocale}.json`, {
method: 'GET',
credentials: 'include',
mode: 'no-cors',
});
if (response.redirected === true || response.status !== 200) return false;
return await response.json();
}
static async translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
}
static async translateElement(element) {
const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) {
let attr = attrs[i];
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
} else {
if (attr.startsWith("data-")) {
let dataAttr = attr.substring(5);
element.dataset.dataAttr = Localization.getTranslation(key, attr);
} {
element.setAttribute(attr, Localization.getTranslation(key, attr));
}
}
}
}
static getTranslation(key, attr=null, data={}, useDefault=false) {
const keys = key.split(".");
let translationCandidates = useDefault
? Localization.defaultTranslations
: Localization.translations;
let translation;
try {
for (let i = 0; i < keys.length - 1; i++) {
translationCandidates = translationCandidates[keys[i]]
}
let lastKey = keys[keys.length - 1];
if (attr) lastKey += "_" + attr;
translation = translationCandidates[lastKey];
for (let j in data) {
translation = translation.replace(`{{${j}}}`, data[j]);
}
} catch (e) {
translation = "";
}
if (!translation) {
if (!useDefault) {
translation = this.getTranslation(key, attr, data, true);
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
} else {
console.warn("Missing translation in default language:", key, attr);
}
}
return Localization.escapeHTML(translation);
}
static escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML;
}
}

View File

@@ -3,27 +3,47 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
Events.on('create-public-room', _ => this._onCreatePublicRoom());
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
Events.on('online', _ => this._connect());
}
_connect() {
clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return;
if (this._isConnected() || this._isConnecting() || this._isOffline()) return;
if (this._isReconnect) {
Events.fire('notify-user', {
message: Localization.getTranslation("notifications.connecting"),
persistent: true
});
}
const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen();
@@ -36,27 +56,47 @@ class ServerConnection {
_onOpen() {
console.log('WS: server connected');
Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
}
_onPairDeviceInitiate() {
if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.');
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
return;
}
this.send({ type: 'pair-device-initiate' })
this.send({ type: 'pair-device-initiate' });
}
_onPairDeviceJoin(roomKey) {
_onPairDeviceJoin(pairKey) {
if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
return;
}
this.send({ type: 'pair-device-join', roomKey: roomKey })
this.send({ type: 'pair-device-join', pairKey: pairKey });
}
_onCreatePublicRoom() {
if (!this._isConnected()) {
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
return;
}
this.send({ type: 'create-public-room' });
}
_onJoinPublicRoom(roomId, createIfInvalid) {
if (!this._isConnected()) {
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
return;
}
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
}
_onLeavePublicRoom() {
if (!this._isConnected()) {
setTimeout(_ => this._onLeavePublicRoom(), 1000);
return;
}
this.send({ type: 'leave-public-room' });
}
_setRtcConfig(config) {
@@ -65,13 +105,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -98,27 +138,58 @@ class ServerConnection {
Events.fire('pair-device-join-key-invalid');
break;
case 'pair-device-canceled':
Events.fire('pair-device-canceled', msg.roomKey);
Events.fire('pair-device-canceled', msg.pairKey);
break;
case 'pair-device-join-key-rate-limit':
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
case 'join-key-rate-limit':
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
break;
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
case 'public-room-id-invalid':
Events.fire('public-room-id-invalid', msg.publicRoomId);
break;
case 'public-room-created':
Events.fire('public-room-created', msg.roomId);
break;
case 'public-room-left':
Events.fire('public-room-left');
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
}
_onDisplayName(msg) {
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem('peer_id', msg.message.peerId);
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -127,8 +198,8 @@ class ServerConnection {
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = sessionStorage.getItem("peerId");
const peerIdHash = sessionStorage.getItem("peerIdHash");
const peerId = sessionStorage.getItem('peer_id');
const peerIdHash = sessionStorage.getItem('peer_id_hash');
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
@@ -138,26 +209,32 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
const peerId = sessionStorage.getItem('peer_id');
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
console.log('WS: server disconnected');
setTimeout(() => {
this._isReconnect = true;
Events.fire('ws-disconnected');
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
}, 100); //delay for 100ms to prevent flickering on page reload
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -169,6 +246,10 @@ class ServerConnection {
return this._socket && this._socket.readyState === this._socket.CONNECTING;
}
_isOffline() {
return !navigator.onLine;
}
_onError(e) {
console.error(e);
}
@@ -181,13 +262,19 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._roomIds = {};
this._updateRoomIds(roomType, roomId);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -198,12 +285,73 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_isPaired() {
return !!this._roomIds['secret'];
}
_getPairSecret() {
return this._roomIds['secret'];
}
_getRoomTypes() {
return Object.keys(this._roomIds);
}
_updateRoomIds(roomType, roomId) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
// multiple roomSecrets with same peer -> delete old roomSecret
PersistentStorage.deleteRoomSecret(this._getPairSecret())
.then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
});
}
this._roomIds[roomType] = roomId;
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._getPairSecret());
}
}
_removeRoomType(roomType) {
delete this._roomIds[roomType];
Events.fire('room-type-removed', {
peerId: this._peerId,
roomType: roomType
});
}
_evaluateAutoAccept() {
if (!this._isPaired()) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry
? roomSecretEntry.entry.auto_accept
: false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = !this._isSameBrowser()
? autoAccept
: false;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -248,7 +396,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -360,7 +512,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -372,6 +524,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -402,7 +562,7 @@ class Peer {
_abortTransfer() {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
Events.fire('notify-user', 'Files are incorrect.');
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
this._filesReceived = [];
this._requestAccepted = null;
this._digester = null;
@@ -443,14 +603,14 @@ class Peer {
this._abortTransfer();
}
// include for compatibility with Snapdrop for Android app
// include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob);
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@@ -460,7 +620,8 @@ class Peer {
this._chunker = null;
if (!this._filesQueue.length) {
this._busy = false;
Events.fire('notify-user', 'File transfer completed.');
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else {
this._dequeueFile();
}
@@ -471,7 +632,7 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
this._filesRequested = null;
if (message.reason === 'ios-memory-limit') {
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
}
return;
}
@@ -481,7 +642,7 @@ class Peer {
}
_onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.');
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
}
sendText(text) {
@@ -497,34 +658,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -534,13 +700,17 @@ class RTCPeer extends Peer {
_openChannel() {
if (!this._conn) return;
const channel = this._conn.createDataChannel('data-channel', {
ordered: true,
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
});
channel.onopen = e => this._onChannelOpened(e);
channel.onerror = e => this._onError(e);
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
this._conn.createOffer()
.then(d => this._onDescription(d))
.catch(e => this._onError(e));
}
_onDescription(description) {
@@ -556,7 +726,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -621,7 +791,7 @@ class RTCPeer extends Peer {
_onBeforeUnload(e) {
if (this._busy) {
e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?";
return Localization.getTranslation("notifications.unfinished-transfers-warning");
}
}
@@ -641,7 +811,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -680,15 +850,19 @@ class RTCPeer extends Peer {
_sendSignal(signal) {
signal.type = 'signal';
signal.to = this._peerId;
signal.roomType = this._roomType;
signal.roomSecret = this._roomSecret;
signal.roomType = this._getRoomTypes()[0];
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(signal);
}
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -716,32 +890,66 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
// this device closes connection
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
// peer closes connection
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
}
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
_refreshPeer(peer, roomType, roomId) {
if (!peer) return false;
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
// if roomType or roomId for roomType differs peer is already connected
// -> only update roomSecret and reevaluate auto accept
if (roomTypesDiffer || roomIdsDiffer) {
peer._updateRoomIds(roomType, roomId);
peer._evaluateAutoAccept();
return true;
}
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomId);
return;
}
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
})
}
@@ -769,10 +977,19 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (msg.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
_onPeerLeft(message) {
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -787,17 +1004,52 @@ class PeersManager {
if (peer._channel) peer._channel.onclose = null;
peer._conn.close();
peer._busy = false;
peer._roomIds = {};
}
_onRoomSecretsDeleted(roomSecrets) {
for (let i=0; i<roomSecrets.length; i++) {
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
}
}
_onLeavePublicRoom(publicRoomId) {
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
}
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
}
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
const peerIds = this._getPeerIdsFromRoomId(roomId);
if (!peerIds.length) return;
for (let i=0; i<peerIds.length; i++) {
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
}
}
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
const peer = this.peers[peerId];
if (!peer) return;
if (peer._getRoomTypes().length > 1) {
peer._removeRoomType(roomType);
} else {
Events.fire('peer-disconnected', peerId);
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -813,6 +1065,31 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdsFromRoomId(roomId) {
if (!roomId) return [];
let peerIds = []
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomId.
if (Object.values(peer._roomIds).includes(roomId)) {
peerIds.push(peer._peerId);
}
}
return peerIds;
}
}
@@ -898,7 +1175,7 @@ class FileDigester {
}
class Events {
static fire(type, detail) {
static fire(type, detail = {}) {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}

View File

@@ -1,39 +1,78 @@
(function(){
// Select the button
const btnTheme = document.getElementById('theme');
// Check for dark mode preference at the OS level
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
// Get the user's theme preference from local storage, if it's available
const currentTheme = localStorage.getItem('theme');
// If the user's preference in localStorage is dark...
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
const $themeAuto = document.getElementById('theme-auto');
const $themeLight = document.getElementById('theme-light');
const $themeDark = document.getElementById('theme-dark');
let currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
// ...let's toggle the .dark-theme class on the body
document.body.classList.toggle('dark-theme');
// Otherwise, if the user's preference in localStorage is light...
setModeToDark();
} else if (currentTheme === 'light') {
// ...let's toggle the .light-theme class on the body
document.body.classList.toggle('light-theme');
setModeToLight();
}
// Listen for a click on the button
btnTheme.addEventListener('click', function(e) {
e.preventDefault();
// If the user's OS setting is dark and matches our .dark-theme class...
let theme;
if (prefersDarkScheme.matches) {
// ...then toggle the light mode class
document.body.classList.toggle('light-theme');
// ...but use .dark-theme if the .light-theme class is already on the body,
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
$themeAuto.addEventListener('click', _ => {
if (currentTheme) {
setModeToAuto();
} else {
// Otherwise, let's do the same thing, but for .dark-theme
document.body.classList.toggle('dark-theme');
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
setModeToDark();
}
});
$themeLight.addEventListener('click', _ => {
if (currentTheme !== 'light') {
setModeToLight();
} else {
setModeToAuto();
}
});
$themeDark.addEventListener('click', _ => {
if (currentTheme !== 'dark') {
setModeToDark();
} else {
setModeToLight();
}
// Finally, let's save the current preference to localStorage to keep using it
localStorage.setItem('theme', theme);
});
function setModeToDark() {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
localStorage.setItem('theme', 'dark');
currentTheme = 'dark';
$themeAuto.classList.remove("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.add("selected");
}
function setModeToLight() {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
localStorage.setItem('theme', 'light');
currentTheme = 'light';
$themeAuto.classList.remove("selected");
$themeLight.classList.add("selected");
$themeDark.classList.remove("selected");
}
function setModeToAuto() {
document.body.classList.remove('dark-theme');
document.body.classList.remove('light-theme');
if (prefersDarkTheme) {
document.body.classList.add('dark-theme');
} else if (prefersLightTheme) {
document.body.classList.add('light-theme');
}
localStorage.removeItem('theme');
currentTheme = undefined;
$themeAuto.classList.add("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.remove("selected");
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -398,3 +398,16 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
function changeFavicon(src) {
document.querySelector('[rel="icon"]').href = src;
document.querySelector('[rel="shortcut icon"]').href = src;
}

View File

@@ -1,16 +1,19 @@
const cacheVersion = 'v1.5.0';
const cacheVersion = 'v1.9.4';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
const urlsToCache = [
'index.html',
'./',
'index.html',
'manifest.json',
'styles.css',
'scripts/localization.js',
'scripts/network.js',
'scripts/NoSleep.min.js',
'scripts/QRCode.min.js',
'scripts/theme.js',
'scripts/ui.js',
'scripts/util.js',
'scripts/qrcode.js',
'scripts/zip.min.js',
'scripts/NoSleep.min.js',
'scripts/theme.js',
'sounds/blop.mp3',
'images/favicon-96x96.png',
'images/favicon-96x96-notification.png',
@@ -19,6 +22,19 @@ const urlsToCache = [
'images/android-chrome-512x512.png',
'images/android-chrome-512x512-maskable.png',
'images/apple-touch-icon.png',
'lang/ar.json',
'lang/de.json',
'lang/en.json',
'lang/es.json',
'lang/fr.json',
'lang/id.json',
'lang/it.json',
'lang/ja.json',
'lang/nb.json',
'lang/nl.json',
'lang/ro.json',
'lang/ru.json',
'lang/zh-CN.json'
];
self.addEventListener('install', function(event) {
@@ -40,7 +56,7 @@ const fromNetwork = (request, timeout) =>
fetch(request).then(response => {
clearTimeout(timeoutId);
fulfill(response);
update(request);
update(request).then(() => console.log("Cache successfully updated for", request.url));
}, reject);
});
@@ -49,9 +65,7 @@ const fromCache = request =>
caches
.open(cacheTitle)
.then(cache =>
cache
.match(request)
.then(matching => matching || cache.match('/offline/'))
cache.match(request)
);
// cache the current page to make it available for offline
@@ -59,63 +73,98 @@ const update = request =>
caches
.open(cacheTitle)
.then(cache =>
fetch(request).then(response => {
cache.put(request, response).then(_ => {
console.log("Page successfully cached.")
fetch(request)
.then(async response => {
await cache.put(request, response);
})
})
.catch(() => console.log(`Cache could not be updated. ${request.url}`))
);
// general strategy when making a request (eg if online try to fetch it
// from the network with a timeout, if something fails serve from cache)
// from cache, if something fails fetch from network. Update cache everytime files are fetched.
// This way files should only be fetched if cacheVersion is changed
self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith(
(async () => {
const formData = await event.request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.get("files");
let share_url = "/";
if (files.length > 0) {
share_url = "/?share-target=files";
const db = await window.indexedDB.open('pairdrop_store');
const tx = db.transaction('share_target_files', 'readwrite');
const store = tx.objectStore('share_target_files');
for (let i=0; i<files.length; i++) {
await store.add(files[i]);
}
await tx.complete
db.close()
} else if (title.length > 0 || text.length > 0 || url.length) {
share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`;
}
return Response.redirect(encodeURI(share_url), 303);
})()
);
event.respondWith((async () => {
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
// Regular requests not related to Web Share Target.
event.respondWith(
fromNetwork(event.request, 10000).catch(() => fromCache(event.request))
);
event.waitUntil(update(event.request));
if (forceFetch) {
event.respondWith(fromNetwork(event.request, 10000));
} else {
event.respondWith(
fromCache(event.request).then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
);
}
}
});
// on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt =>
evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
)
self.addEventListener('activate', evt => {
return evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
)
}
);
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
fileObjects.push({
name: files[i].name,
buffer: await files[i].arrayBuffer()
});
}
const DBOpenRequest = indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
for (let i = 0; i < fileObjects.length; i++) {
const transaction = db.transaction('share_target_files', 'readwrite');
const objectStore = transaction.objectStore('share_target_files');
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve(pairDropUrl);
}
} else {
let urlArgument = '?share-target=text';
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(pairDropUrl + urlArgument);
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no">
@@ -33,114 +33,301 @@
<meta name="twitter:image" content="images/logo_transparent_512x512.png">
<meta property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources -->
<link rel="preload" href="lang/en.json" as="fetch">
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="manifest" href="manifest.json">
</head>
<body translate="no">
<header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop">
<header class="row-reverse opacity-0">
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label">
<svg class="icon">
<use xlink:href="#info-outline" />
<use xlink:href="#info-outline"></use>
</svg>
</a>
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme" />
<use xlink:href="#icon-language-selector"></use>
</svg>
</a>
<a id="notification" class="icon-button" title="Enable Notifications" hidden>
</div>
<div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-auto"></use>
</svg>
</div>
<div>
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-light"></use>
</svg>
</div>
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-dark"></use>
</svg>
</div>
</div>
</div>
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#notifications" />
<use xlink:href="#notifications"></use>
</svg>
</a>
<a id="install" class="icon-button" title="Install PairDrop" hidden>
</div>
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#homescreen" />
<use xlink:href="#homescreen"></use>
</svg>
</a>
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
</div>
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#pair-device-icon" />
<use xlink:href="#pair-device-icon"></use>
</svg>
</a>
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
</div>
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon"></use>
</svg>
</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</div>
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#public-room-icon"></use>
</svg>
</div>
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden></div>
</header>
<!-- Center -->
<div id="center">
<div id="center" class="opacity-0">
<!-- Peers -->
<div class="x-peers-filler"></div>
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
<x-no-peers class="no-animation-on-load" data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg">
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text"></h2>
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text"></div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg">
<p id="paste-filename"></p>
</x-instructions>
<div id="websocket-fallback">
<span data-i18n-key="footer.traffic" data-i18n-attrs="text"></span>
<span data-i18n-key="footer.routed" data-i18n-attrs="text"></span>
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text"></span>
</div>
</div>
<!-- Footer -->
<footer class="column">
<footer class="column opacity-0">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
<use xlink:href="#wifi-tethering"></use>
</svg>
<div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
</div>
<div id="websocket-fallback">
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
<div class="column">
<div class="known-as-wrapper">
<span data-i18n-key="footer.known-as" data-i18n-attrs="text"></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>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon"></use>
</svg>
</div>
<div class="discovery-wrapper row">
<div class="row center">
<span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span>
</div>
<div class="row center">
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
</div>
</div>
</div>
</footer>
<!-- Language Select Dialog -->
<x-dialog id="language-select-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2>
</div>
<div class="language-buttons">
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></button>
<button class="button fw" value="ar">
<span>العربية</span>
<span>-</span>
<span>(Arabic)</span>
</button>
<button class="button fw" value="de">
<span>Deutsch</span>
<span>-</span>
<span>(German)</span>
</button>
<button class="button fw" value="en">
<span>English</span>
</button>
<button class="button fw" value="fr">
<span>Français</span>
<span>-</span>
<span>(French)</span>
</button>
<button class="button fw" value="id">
<span>Bahasa Indonesia</span>
<span>-</span>
<span>(Indonesian)</span>
</button>
<button class="button fw" value="it">
<span>Italiano</span>
<span>-</span>
<span>(Italian)</span>
</button>
<button class="button fw" value="nl">
<span>Nederlands</span>
<span>-</span>
<span>(Dutch)</span>
</button>
<button class="button fw" value="nb">
<span>Norsk</span>
<span>-</span>
<span>(Norwegian)</span>
</button>
<button class="button fw" value="ro">
<span>Română</span>
<span>-</span>
<span>(Romanian)</span>
</button>
<button class="button fw" value="ru">
<span>Русский язык</span>
<span>-</span>
<span>(Russian)</span>
</button>
<button class="button fw" value="es">
<span>Español</span>
<span>-</span>
<span>(Spanish)</span>
</button>
<button class="button fw" value="zh-CN">
<span>中文</span>
<span>-</span>
<span>(Chinese)</span>
</button>
<button class="button fw" value="ja">
<span>日本語</span>
<span>-</span>
<span>(Japanese)</span>
</button>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Pair Device Dialog -->
<x-dialog id="pair-device-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Pair Devices</h2>
<div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<div id="key-input-container">
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" type="button" close>Cancel</button>
<div class="row center">
<div class="column">
<div class="center key-qr-code" data-i18n-key="dialogs.pair-devices-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr">000 000</h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column">
<div class="input-key-container six-chars" dir="ltr">
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<div class="row center">
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty"></div>
<div class="font-subheading center">
<p>
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text"></span>
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text"></u>
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text"></span>
</p>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Public Room Dialog -->
<x-dialog id="public-room-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<div class="row center">
<div class="column">
<h2 class="center" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row center">
<div class="column">
<div class="center key-qr-code" data-i18n-key="dialogs.public-room-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr"></h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column">
<div class="input-key-container" dir="ltr">
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="center row-reverse button-row">
<button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -150,24 +337,30 @@
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>would like to share</span>
<div class="row center">
<div class="column">
<h2 class="center"></h2>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row center">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
<div class="row-reverse center button-row">
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus></button>
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -176,24 +369,31 @@
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>has sent</span>
<div class="row center">
<div class="column">
<h2 class="center"></h2>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row center">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
<div class="row-reverse center button-row">
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus></button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@@ -203,16 +403,27 @@
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span class="display-name"></span>
<div class="row center">
<div class="column">
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
<div class="row center display-name-wrapper">
<div class="column">
<div class="text-center">
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text"></span>
<span class="display-name badge"></span>
</div>
</div>
</div>
<div class="row">
<div class="column fw">
<div id="text-input" class="textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@@ -222,16 +433,23 @@
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
<div class="row center">
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button>
<div class="row center">
<div class="text-center">
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="column fw">
<div id="text" class="textarea fw"></div>
</div>
</div>
<div class="row-reverse center button-row">
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text"></button>
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@@ -240,119 +458,143 @@
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button>
<div class="row-reverse center button-row">
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast id="toast" class="row" shadow="1"></x-toast>
<x-toast id="toast" class="row center" shadow="1"></x-toast>
</div>
<!-- About Page -->
<x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<header class="row-reverse">
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label">
<svg class="icon">
<use xlink:href="#close-icon" />
<use xlink:href="#close-icon"></use>
</svg>
</a>
</header>
<section class="center column fade-in">
<section class="center column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
<use xlink:href="#wifi-tethering"></use>
</svg>
<h1>PairDrop</h1>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.9.4</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#github" />
<use xlink:href="#github"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#monetarization" />
<use xlink:href="#monetarization"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&amp;" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#twitter" />
<use xlink:href="#twitter"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#help-outline" />
<use xlink:href="#help-outline"></use>
</svg>
</a>
</div>
</section>
<x-background></x-background>
</x-about>
<canvas class="circles opacity-0"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
<symbol id="wifi-tethering" viewBox="0 0 24 24">
<path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z"></path>
</symbol>
<symbol id=desktop-mac viewBox="0 0 24 24">
<symbol id="desktop-mac" viewBox="0 0 24 24">
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z"></path>
</symbol>
<symbol id=phone-iphone viewBox="0 0 24 24">
<symbol id="phone-iphone" viewBox="0 0 24 24">
<path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"></path>
</symbol>
<symbol id=tablet-mac viewBox="0 0 24 24">
<symbol id="tablet-mac" viewBox="0 0 24 24">
<path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z"></path>
</symbol>
<symbol id=info-outline viewBox="0 0 24 24">
<symbol id="info-outline" viewBox="0 0 24 24">
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
</symbol>
<symbol id=close-icon viewBox="0 0 24 24">
<symbol id="close-icon" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</symbol>
<symbol id=help-outline viewBox="0 0 24 24">
<symbol id="help-outline" viewBox="0 0 24 24">
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
</symbol>
<symbol id="twitter">
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z"></path>
</symbol>
<symbol id="github">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</symbol>
<g id="notifications">
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"></path>
</g>
<symbol id="homescreen">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z" />
<path fill="none" d="M0 0h24v24H0V0z" />
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path>
</symbol>
<symbol id="monetarization">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path>
</symbol>
<symbol id="icon-theme" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
<symbol id="icon-theme-auto" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"></path></svg>
</symbol>
<symbol id="icon-theme-light" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"></path></svg>
</symbol>
<symbol id="icon-theme-dark" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"></rect><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path>
</symbol>
<symbol id="pair-device-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"></path>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"></path>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"></path>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"></path>
</symbol>
<symbol id="public-room-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"></path>
</symbol>
<symbol id="icon-language-selector" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"></path>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/util.js"></script>
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script>
<script src="scripts/theme.js" async></script>
<script src="scripts/qrcode.js" async></script>
<script src="scripts/util.js"></script>
<script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script>
<!-- Sounds -->
@@ -368,3 +610,4 @@
</x-noscript>
</noscript>
</body>
</html>

View File

@@ -0,0 +1,159 @@
{
"footer": {
"webrtc": "إذا لم يكن WebRTC متاحًا.",
"public-room-devices_title": "يمكن اكتشافك بواسطة الأجهزة الموجودة في هذه الغرفة العامة المستقلة عن الشبكة.",
"display-name_data-placeholder": "تحميل …",
"display-name_title": "قم بتحرير اسم جهازك بشكل دائم",
"traffic": "حركة المرور هي",
"paired-devices_title": "يمكن اكتشافك بواسطة الأجهزة المقترنة في جميع الأوقات بشكل مستقل عن الشبكة.",
"public-room-devices": "في الغرفة {{roomId}}",
"paired-devices": "بواسطة الأجهزة المقترنة",
"on-this-network": "على هذه الشبكة",
"routed": "توجيهّا من خلال الخادم",
"discovery": "يمكنك اكتشاف:",
"on-this-network_title": "يمكن للجميع اكتشافك على هذه الشبكة.",
"known-as": "‌أنت معروف بأنك:"
},
"notifications": {
"request-title": "يرغب {{name}} في نقل {{count}} {{descriptor}}",
"unfinished-transfers-warning": "هناك تحويلات غير مكتملة. هل أنت متأكد أنك تريد إغلاق PairDrop؟",
"message-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"rate-limit-join-key": "تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.",
"connecting": "يتصل …",
"pairing-key-invalidated": "المفتاح {{key}} خاطئ.",
"pairing-key-invalid": "مُفتاح خاطئ",
"connected": "متصل.",
"pairing-not-persistent": "الأجهزة المقترنة ليست ثابتة.",
"text-content-incorrect": "محتوى النص غير صحيح.",
"message-transfer-completed": "اكتمل نقل الرسالة.",
"file-transfer-completed": "اكتمل نقل الملف.",
"file-content-incorrect": "محتوى الملف غير صحيح.",
"files-incorrect": "الملفات غير صحيحة.",
"selected-peer-left": "مُحَدد الاجهزة المقترنة.",
"link-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"online": "لقد عدت متصلاً بالإنترنت",
"public-room-left": "الخروج من الغرفة العامة {{publicRoomId}}",
"copied-text": "نُسِخَ النص إلى الحافظة",
"display-name-random-again": "يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى.",
"display-name-changed-permanently": "يتم تغيير اسم العرض بشكل دائم.",
"copied-to-clipboard-error": "النسخ غير ممكن. انسخ يدويًا.",
"pairing-success": "الأجهزة المقترنة.",
"clipboard-content-incorrect": "محتوى الحافظة غير صحيح.",
"display-name-changed-temporarily": "تم تغيير اسم العرض لهذه الجلسة فقط.",
"copied-to-clipboard": "تم النسخ إلى الحافظة",
"offline": "انت غير متصل",
"pairing-tabs-error": "من المستحيل إقران علامتي تبويب متصفح الويب.",
"public-room-id-invalid": "معرف الغرفة غير صالح",
"click-to-download": "إضغط للتحميل",
"pairing-cleared": "جميع الأجهزة غير مقترنة.",
"notifications-enabled": "تم تمكين الإشعارات.",
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة.",
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة.",
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
"download-successful": "تم تحميل {{descriptor}}",
"click-to-show": "اضغط للعرض"
},
"header": {
"cancel-paste-mode": "تمّ",
"theme-auto_title": "تكيٌف المظهر مع النظام",
"install_title": "تثبيت PairDrop",
"theme-dark_title": "إستخدام دائما المظهر المظلم",
"pair-device_title": "قم بإقران أجهزتك بشكل دائم",
"join-public-room_title": "انضم إلى الغرفة العامة مؤقتًا",
"notification_title": "تشغيل الإشعارات",
"edit-paired-devices_title": "تعديل الأجهزة المقترنة",
"language-selector_title": "إختر اللغةعربي",
"about_title": "حول PairDrop",
"about_aria-label": "افتح حول PairDrop",
"theme-light_title": "إستخدم دائماً المظهر الفاتح"
},
"instructions": {
"x-instructions_mobile": "انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة",
"click-to-send": "انقر للإرسال",
"activate-paste-mode-and-other-files": "و{{count}} ملفات أخرى",
"tap-to-send": "انقر للإرسال",
"activate-paste-mode-base": "افتح PairDrop على الأجهزة الأخرى للإرسال",
"no-peers-subtitle": "قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من إكتشافها على الشبكات الأخرى",
"activate-paste-mode-shared-text": "النص المشترك",
"x-instructions_desktop": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"no-peers-title": "افتح PairDrop على الأجهزة الأخرى لإرسال الملفات",
"x-instructions_data-drop-bg": "حرر لتحديد المستلم",
"no-peers_data-drop-bg": "حرر لتحديد المستلم",
"x-instructions_data-drop-peer": "قم بالتحرير لإرسالها إلى النظير"
},
"peer-ui": {
"processing": "مُعالجة …",
"click-to-send-paste-mode": "انقر للإرسال {{descriptor}}",
"click-to-send": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"waiting": "يُرجى الإنتظار…",
"connection-hash": "للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين",
"preparing": "يقترن…",
"transferring": "جارٍ النقل…"
},
"dialogs": {
"base64-paste-to-send": "الصق هنا لإرسال {{type}}",
"auto-accept-instructions-2": "لقبول جميع الملفات المرسلة من هذا الجهاز تلقائيًا.",
"receive-text-title": "تلقيت رسالة",
"edit-paired-devices-title": "تحرير الأجهزة المقترنة",
"cancel": "إلغاء",
"auto-accept-instructions-1": "تفعيل",
"pair-devices-title": "إقران الأجهزة بشكل دائم",
"download": "تحميل",
"title-file": "ملف",
"base64-processing": "مُعالجة…",
"decline": "رفض",
"receive-title": "تم الاستلام {{descriptor}}",
"leave": "مُغادرة",
"join": "انضمام",
"title-image-plural": "صور",
"send": "ارسال",
"base64-tap-to-paste": "انقر هنا للصق {{type}}",
"base64-text": "نص",
"copy": "نسخ",
"file-other-description-image": "وصورة واحدة أخرى",
"temporary-public-room-title": "غرفة عامة مؤقتة",
"base64-files": "ملفات",
"has-sent": "ارسلت:",
"file-other-description-file": "وملف واحد آخر",
"close": "إغلاق",
"system-language": "لغة النظام",
"unpair": "إلغاء الإقتران",
"title-image": "صورة",
"file-other-description-file-plural": "و{{count}} ملفات أخرى",
"would-like-to-share": "ترغب في المشاركة",
"send-message-to": "أرسال رسالة إلى",
"language-selector-title": "إختر اللُغة",
"pair": "إقتران",
"hr-or": "او",
"scan-qr-code": "أو مسح رمز الاستجابة السريعة.",
"input-key-on-this-device": "أدخل هذا المفتاح على جهاز آخر",
"download-again": "تحميل مرة أخرى",
"accept": "قبول",
"paired-devices-wrapper_data-empty": "لا توجد أجهزة مقترنة.",
"enter-key-from-another-device": "أدخل المفتاح من جهاز آخر هنا.",
"share": "مُشاركة",
"auto-accept": "قبول تلقائي",
"title-file-plural": "ملفات",
"send-message-title": "إرسال رسالة",
"input-room-id-on-another-device": "‌أدخل معرف الغرفة هذا على جهاز آخر ما ",
"file-other-description-image-plural": "و{{count}} صور أخرى",
"enter-room-id-from-another-device": "أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة."
},
"about": {
"claim": "أسهل طريقة لنقل الملفات عبر الأجهزة",
"tweet_title": "غرّد حول PairDrop",
"close-about_aria-label": "إغلاق حول PairDrop",
"buy-me-a-coffee_title": "اشتري لي القهوة!",
"github_title": "PairDrop على جيت هاب",
"faq_title": "أسئلة متكررة"
},
"document-titles": {
"file-transfer-requested": "طلب نقل الملف",
"message-received-plural": "{{count}} الرسائل المستلمة",
"message-received": "تم إرسال الرسالة",
"file-received": "تم استلام الملف",
"file-received-plural": "{{count}} الملفات المستلمة",
"image-transfer-requested": "طُلب نقل الصور المطلوبة"
}
}

View File

@@ -0,0 +1,167 @@
{
"header": {
"about_title": "Über PairDrop",
"notification_title": "Benachrichtigungen aktivieren",
"about_aria-label": "Über PairDrop öffnen",
"install_title": "PairDrop installieren",
"pair-device_title": "Deine Geräte dauerhaft koppeln",
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
"theme-auto_title": "Systemstil verwenden",
"theme-dark_title": "Immer dunklen Stil verwenden",
"theme-light_title": "Immer hellen Stil verwenden",
"cancel-paste-mode": "Fertig",
"language-selector_title": "Sprache Wählen",
"join-public-room_title": "Öffentlichen Raum temporär betreten"
},
"dialogs": {
"share": "Teilen",
"download": "Herunterladen",
"pair-devices-title": "Geräte Dauerhaft Koppeln",
"input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein",
"enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.",
"pair": "Koppeln",
"cancel": "Abbrechen",
"edit-paired-devices-title": "Gekoppelte Geräte Bearbeiten",
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
"close": "Schließen",
"accept": "Akzeptieren",
"decline": "Ablehnen",
"title-image": "Bild",
"title-file": "Datei",
"title-image-plural": "Bilder",
"title-file-plural": "Dateien",
"scan-qr-code": "oder scanne den QR-Code.",
"would-like-to-share": "möchte Folgendes teilen",
"send": "Senden",
"copy": "Kopieren",
"receive-text-title": "Textnachricht Erhalten",
"file-other-description-image-plural": "und {{count}} andere Bilder",
"file-other-description-file-plural": "und {{count}} andere Dateien",
"auto-accept-instructions-1": "Aktiviere",
"auto-accept": "automatisch-akzeptieren",
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
"has-sent": "hat Folgendes gesendet:",
"send-message-title": "Textnachricht Senden",
"send-message-to": "Sende eine Textnachricht an",
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
"base64-text": "Text",
"base64-files": "Dateien",
"base64-processing": "Bearbeitung läuft…",
"file-other-description-image": "und ein anderes Bild",
"file-other-description-file": "und eine andere Datei",
"receive-title": "{{descriptor}} Erhalten",
"download-again": "Erneut herunterladen",
"system-language": "Systemsprache",
"language-selector-title": "Sprache Einstellen",
"hr-or": "ODER",
"input-room-id-on-another-device": "Gib diese Raum-ID auf einem anderen Gerät ein",
"unpair": "Entkoppeln",
"leave": "Verlassen",
"join": "Betreten",
"enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.",
"temporary-public-room-title": "Temporärer Öffentlicher Raum",
"message_title": "Nachricht zum Senden hier einfügen",
"pair-devices-qr-code_title": "Klicke, um Link zum Koppeln mit diesem Gerät zu kopieren",
"public-room-qr-code_title": "Klicke, um Link zu diesem öffentlichen Raum zu kopieren"
},
"about": {
"tweet_title": "Über PairDrop twittern",
"faq_title": "Häufig gestellte Fragen",
"close-about_aria-label": "Schließe Über PairDrop",
"github_title": "PairDrop auf GitHub",
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
"claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen"
},
"footer": {
"known-as": "Du wirst angezeigt als:",
"display-name_title": "Setze einen permanenten Gerätenamen",
"on-this-network": "in diesem Netzwerk",
"paired-devices": "für gekoppelte Geräte",
"traffic": "Datenverkehr wird",
"display-name_placeholder": "Lade…",
"routed": "durch den Server geleitet",
"webrtc": "wenn WebRTC nicht verfügbar ist.",
"display-name_data-placeholder": "Lade…",
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum gefunden werden, unabhängig von deinem Netzwerk.",
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
"public-room-devices": "in Raum {{roomId}}",
"discovery": "Du bist sichtbar:",
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
},
"notifications": {
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
"click-to-download": "Klicken zum Download",
"copied-text": "Text in die Zwischenablage kopiert",
"connected": "Verbunden.",
"pairing-success": "Geräte gekoppelt.",
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
"pairing-key-invalid": "Ungültiger Schlüssel",
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
"copied-to-clipboard": "In die Zwischenablage kopiert",
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
"notifications-enabled": "Benachrichtigungen aktiviert.",
"offline": "Du bist offline",
"online": "Du bist wieder Online",
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
"download-successful": "{{descriptor}} heruntergeladen",
"pairing-cleared": "Alle Geräte entkoppelt.",
"click-to-show": "Klicken zum Anzeigen",
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert.",
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
"connecting": "Verbindung wird hergestellt…",
"files-incorrect": "Dateien sind fehlerhaft.",
"file-transfer-completed": "Dateiübertragung abgeschlossen.",
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
"public-room-id-invalid": "Ungültige Raum-ID",
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können.",
"notifications-permissions-error": "Benachrichtigungen wurden blockiert, weil der Nutzer die Berechtigungsanfrage mehrfach abgelehnt hat. Dies kann in den Einstellungen der Website zurückgesetzt werden, welche durch Klick auf das Schloss Symbol neben der URL Leiste erreicht werden können.",
"pair-url-copied-to-clipboard": "Link zum Koppeln mit diesem Gerät in Zwischenablage kopiert",
"room-url-copied-to-clipboard": "Link zu diesem öffentlichen Raum in Zwischenablage kopiert"
},
"instructions": {
"x-instructions_desktop": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
"click-to-send": "Klicke zum Senden von",
"tap-to-send": "Tippe zum Senden von",
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
"x-instructions_mobile": "Tippe, um Dateien zu senden oder tippe lange, um Nachrichten zu senden",
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
"activate-paste-mode-shared-text": "freigegebenem Text"
},
"document-titles": {
"file-transfer-requested": "Dateiübertragung angefordert",
"file-received": "Datei erhalten",
"file-received-plural": "{{count}} Dateien erhalten",
"message-received": "Nachricht erhalten",
"message-received-plural": "{{count}} Nachrichten erhalten",
"image-transfer-requested": "Bilder Transfer beantragt"
},
"peer-ui": {
"click-to-send": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
"waiting": "Warte…",
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
"transferring": "Übertragung läuft…",
"processing": "Bearbeitung läuft…",
"preparing": "Vorbereitung läuft…"
}
}

View File

@@ -0,0 +1,165 @@
{
"header": {
"about_title": "About PairDrop",
"language-selector_title": "Set Language",
"about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt theme to system automatically",
"theme-light_title": "Always use light theme",
"theme-dark_title": "Always use dark theme",
"notification_title": "Enable notifications",
"install_title": "Install PairDrop",
"pair-device_title": "Pair your devices permanently",
"edit-paired-devices_title": "Edit paired devices",
"join-public-room_title": "Join public room temporarily",
"cancel-paste-mode": "Done"
},
"instructions": {
"no-peers_data-drop-bg": "Release to select recipient",
"no-peers-title": "Open PairDrop on other devices to send files",
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
"x-instructions_desktop": "Click to send files or right click to send a message",
"x-instructions_mobile": "Tap to send files or long tap to send a message",
"x-instructions_data-drop-peer": "Release to send to peer",
"x-instructions_data-drop-bg": "Release to select recipient",
"click-to-send": "Click to send",
"tap-to-send": "Tap to send",
"activate-paste-mode-base": "Open PairDrop on other devices to send",
"activate-paste-mode-and-other-files": "and {{count}} other files",
"activate-paste-mode-shared-text": "shared text"
},
"footer": {
"known-as": "You are known as:",
"display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently",
"discovery": "You can be discovered:",
"on-this-network": "on this network",
"on-this-network_title": "You can be discovered by everyone on this network.",
"paired-devices": "by paired devices",
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
"public-room-devices": "in room {{roomId}}",
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
"traffic": "Traffic is",
"routed": "routed through the server",
"webrtc": "if WebRTC is not available."
},
"dialogs": {
"pair-devices-title": "Pair Devices Permanently",
"input-key-on-this-device": "Input this key on another device",
"scan-qr-code": "or scan the QR-code.",
"enter-key-from-another-device": "Enter key from another device here.",
"temporary-public-room-title": "Temporary Public Room",
"input-room-id-on-another-device": "Input this room ID on another device",
"enter-room-id-from-another-device": "Enter room ID from another device to join room.",
"hr-or": "OR",
"pair": "Pair",
"cancel": "Cancel",
"edit-paired-devices-title": "Edit Paired Devices",
"unpair": "Unpair",
"paired-devices-wrapper_data-empty": "No paired devices.",
"auto-accept-instructions-1": "Activate",
"auto-accept": "auto-accept",
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
"close": "Close",
"join": "Join",
"leave": "Leave",
"would-like-to-share": "would like to share",
"accept": "Accept",
"decline": "Decline",
"has-sent": "has sent:",
"share": "Share",
"download": "Download",
"send-message-title": "Send Message",
"send-message-to": "Send a Message to",
"message_title": "Insert message to send",
"send": "Send",
"receive-text-title": "Message Received",
"copy": "Copy",
"base64-processing": "Processing…",
"base64-tap-to-paste": "Tap here to paste {{type}}",
"base64-paste-to-send": "Paste here to send {{type}}",
"base64-text": "text",
"base64-files": "files",
"file-other-description-image": "and 1 other image",
"file-other-description-file": "and 1 other file",
"file-other-description-image-plural": "and {{count}} other images",
"file-other-description-file-plural": "and {{count}} other files",
"title-image": "Image",
"title-file": "File",
"title-image-plural": "Images",
"title-file-plural": "Files",
"receive-title": "{{descriptor}} Received",
"download-again": "Download again",
"language-selector-title": "Set Language",
"system-language": "System Language",
"public-room-qr-code_title": "Click to copy link to public room",
"pair-devices-qr-code_title": "Click to copy link to pair this device"
},
"about": {
"close-about_aria-label": "Close About PairDrop",
"claim": "The easiest way to transfer files across devices",
"github_title": "PairDrop on GitHub",
"buy-me-a-coffee_title": "Buy me a coffee!",
"tweet_title": "Tweet about PairDrop",
"faq_title": "Frequently asked questions"
},
"notifications": {
"display-name-changed-permanently": "Display name is changed permanently.",
"display-name-changed-temporarily": "Display name is changed only for this session.",
"display-name-random-again": "Display name is randomly generated again.",
"download-successful": "{{descriptor}} downloaded",
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
"pairing-success": "Devices paired.",
"pairing-not-persistent": "Paired devices are not persistent.",
"pairing-key-invalid": "Invalid key",
"pairing-key-invalidated": "Key {{key}} invalidated.",
"pairing-cleared": "All Devices unpaired.",
"public-room-id-invalid": "Invalid room ID",
"public-room-left": "Left public room {{publicRoomId}}",
"copied-to-clipboard": "Copied to clipboard",
"pair-url-copied-to-clipboard": "Link to pair this device copied to clipboard",
"room-url-copied-to-clipboard": "Link to public room copied to clipboard",
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
"text-content-incorrect": "Text content is incorrect.",
"file-content-incorrect": "File content is incorrect.",
"clipboard-content-incorrect": "Clipboard content is incorrect.",
"notifications-enabled": "Notifications enabled.",
"notifications-permissions-error": "Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL bar.",
"link-received": "Link received by {{name}} - Click to open",
"message-received": "Message received by {{name}} - Click to copy",
"click-to-download": "Click to download",
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
"click-to-show": "Click to show",
"copied-text": "Copied text to clipboard",
"copied-text-error": "Writing to clipboard failed. Copy manually!",
"offline": "You are offline",
"online": "You are back online",
"connected": "Connected.",
"online-requirement-pairing": "You need to be online to pair devices.",
"online-requirement-public-room": "You need to be online to create a public room.",
"connecting": "Connecting…",
"files-incorrect": "Files are incorrect.",
"file-transfer-completed": "File transfer completed.",
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
"message-transfer-completed": "Message transfer completed.",
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
"selected-peer-left": "Selected peer left."
},
"document-titles": {
"file-received": "File Received",
"file-received-plural": "{{count}} Files Received",
"file-transfer-requested": "File Transfer Requested",
"image-transfer-requested": "Image Transfer Requested",
"message-received": "Message Received",
"message-received-plural": "{{count}} Messages Received"
},
"peer-ui": {
"click-to-send-paste-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"preparing": "Preparing…",
"waiting": "Waiting…",
"processing": "Processing…",
"transferring": "Transferring…"
}
}

View File

@@ -0,0 +1,165 @@
{
"header": {
"theme-auto_title": "Adaptar tema al sistema",
"language-selector_title": "Configurar Idioma",
"about_title": "Sobre PairDrop",
"about_aria-label": "Abrir Sobre PairDrop",
"cancel-paste-mode": "Listo",
"install_title": "Instalar PairDrop",
"theme-dark_title": "Siempre usar tema oscuro",
"pair-device_title": "Empareja tus dispositivos permanentemente",
"join-public-room_title": "Unirse a una sala pública temporalmente",
"notification_title": "Activar notificaciones",
"edit-paired-devices_title": "Editar dispositivos emparejados",
"theme-light_title": "Siempre usar tema claro"
},
"footer": {
"webrtc": "si WebRTC no está disponible.",
"public-room-devices_title": "Puedes ser descubierto por dispositivos en esta sala pública independientemente de la red.",
"display-name_data-placeholder": "Cargando…",
"display-name_title": "Edita el nombre de tu dispositivo de forma permanente",
"traffic": "El tráfico es",
"paired-devices_title": "Puedes ser descubierto por los dispositivos emparejados todo el tiempo independientemente de la red.",
"public-room-devices": "en la sala {{roomId}}",
"paired-devices": "por dispositivos emparejados",
"on-this-network": "en esta red",
"routed": "enrutado a través del servidor",
"discovery": "Puedes ser descubierto:",
"on-this-network_title": "Puedes ser descubierto por todos en esta red.",
"known-as": "Eres conocido como:"
},
"notifications": {
"request-title": "{{name}} quiere transferir {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Hay transferencias no terminadas. ¿Estás seguro de que quieres cerrar PairDrop?",
"message-received": "Mensaje recibido por {{name}} - Haga clic para copiar",
"rate-limit-join-key": "Límite de intentos alcanzado. Espere 10 segundos y vuelva a intentarlo.",
"connecting": "Conectando…",
"pairing-key-invalidated": "Clave {{key}} invalidada.",
"pairing-key-invalid": "Clave inválida",
"connected": "Connectado.",
"pairing-not-persistent": "Los dispositivos emparejados no son persistentes.",
"text-content-incorrect": "El contenido del texto es incorrecto.",
"message-transfer-completed": "Transferencia de mensaje completada.",
"file-transfer-completed": "Transferencia de archivos completada.",
"file-content-incorrect": "El contenido del archivo es incorrecto.",
"files-incorrect": "Los archivos son incorrectos.",
"selected-peer-left": "El dispositivo seleccionado se fue.",
"link-received": "Link recibido por {{name}} - Haga clic para abrir",
"online": "Estás de nuevo en línea",
"public-room-left": "Salió de la sala pública {{publicRoomId}}",
"copied-text": "Texto copiado al portapapeles",
"display-name-random-again": "El nombre mostrado se genera aleatoriamente nuevamente.",
"display-name-changed-permanently": "El nombre para mostrar se ha cambiado permanentemente.",
"copied-to-clipboard-error": "No es posible copiarlo. Cópielo manualmente.",
"pairing-success": "Dispositivos emparejados.",
"clipboard-content-incorrect": "El contenido del portapapeles es incorrecto.",
"display-name-changed-temporarily": "El nombre mostrado se cambia solo para esta sesión.",
"copied-to-clipboard": "Copiado al portapapeles",
"offline": "Estás desconectado",
"pairing-tabs-error": "Emparejar dos pestañas del navegador es imposible.",
"public-room-id-invalid": "ID de sala no válido",
"click-to-download": "Haga clic para descargar",
"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",
"click-to-show": "Click para mostrar",
"notifications-permissions-error": "Las notificaciones se bloquearon porque el usuario rechazó la solicitud del permiso varias veces. Esto se puede restablecer en la configuración de la página web, a la que se quiere acceder haciendo clic en el icono del candado al lado de la barra con la URL.",
"pair-url-copied-to-clipboard": "El enlace para emparejar este dispositivo se copió en el portapapeles",
"room-url-copied-to-clipboard": "El enlace a la sala pública se copió en el portapapeles"
},
"instructions": {
"x-instructions_mobile": "Toque para enviar archivos o toque prologádamente para enviar un mensaje",
"click-to-send": "Haga clic para enviar",
"activate-paste-mode-and-other-files": "y {{count}} archivos diferentes",
"tap-to-send": "Toca para enviar",
"activate-paste-mode-base": "Abra PairDrop en otros dispositivos para enviar",
"no-peers-subtitle": "Empareje dispositivos o ingrese a una sala pública para que lo puedan encontrar en otras redes",
"activate-paste-mode-shared-text": "texto compartido",
"x-instructions_desktop": "Haga clic para enviar archivos o haga clic derecho para enviar un mensaje",
"no-peers-title": "Abra PairDrop en otros dispositivos para enviar archivos",
"x-instructions_data-drop-peer": "Liberar para enviar a un par",
"x-instructions_data-drop-bg": "Liberar para seleccionar destinatario",
"no-peers_data-drop-bg": "Liberar para seleccionar destinatario"
},
"peer-ui": {
"processing": "Procesando…",
"click-to-send-paste-mode": "Haga clic para enviar {{descriptor}}",
"click-to-send": "Haga clic para enviar archivos o haga clic derecho para enviar un mensaje",
"waiting": "Esperando…",
"connection-hash": "Para verificar la seguridad del cifrado de extremo a extremo, compare este número de seguridad en ambos dispositivos",
"preparing": "Preparando…",
"transferring": "Transferiendo…"
},
"dialogs": {
"base64-paste-to-send": "Pegar aquí para enviar {{type}}",
"auto-accept-instructions-2": "para aceptar automáticamente todos los archivos enviados desde ese dispositivo.",
"receive-text-title": "Mensaje Recibido",
"edit-paired-devices-title": "Editar Dispositivos Emparejados",
"cancel": "Cancelar",
"auto-accept-instructions-1": "Activar",
"pair-devices-title": "Emparejar dispositivos permanentemente",
"download": "Descargar",
"title-file": "Archivo",
"base64-processing": "Procesando…",
"decline": "Rechazar",
"receive-title": "{{descriptor}} Recibido",
"leave": "Salir",
"join": "Unirse",
"title-image-plural": "Imágenes",
"send": "Enviar",
"base64-tap-to-paste": "Toca aquí para pegar {{type}}",
"base64-text": "texto",
"copy": "Copiar",
"file-other-description-image": "y una imagen mas",
"temporary-public-room-title": "Sala pública temporal",
"base64-files": "archivos",
"has-sent": "ha enviado:",
"file-other-description-file": "y otro archivo",
"close": "Cerrar",
"system-language": "Idioma del Sistema",
"unpair": "Desemparejar",
"title-image": "Imagen",
"file-other-description-file-plural": "y {{count}} archivos más",
"would-like-to-share": "quisiera compartir",
"send-message-to": "Enviar un Mensaje a",
"language-selector-title": "Configurar Idioma",
"pair": "Emparejar",
"hr-or": "O",
"scan-qr-code": "o escanea el código QR.",
"input-key-on-this-device": "Ingrese esta clave en otro dispositivo",
"download-again": "Descargar de nuevo",
"accept": "Aceptar",
"paired-devices-wrapper_data-empty": "Sin dispositivos emparejados.",
"enter-key-from-another-device": "Ingresa la clave de otro dispositivo aquí.",
"share": "Compartir",
"auto-accept": "aceptar automáticamente",
"title-file-plural": "Archivos",
"send-message-title": "Enviar Mensaje",
"input-room-id-on-another-device": "Ingrese el ID de esta sala en otro dispositivo",
"file-other-description-image-plural": "y {{count}} imágenes más",
"enter-room-id-from-another-device": "Ingresa el ID de la sala desde otro dispositivo para unirte a la sala.",
"message_title": "Insertar el mensaje a enviar",
"pair-devices-qr-code_title": "Haz clic para copiar el enlace para emparejar este dispositivo",
"public-room-qr-code_title": "Haz clic para copiar el enlace a la sala pública"
},
"about": {
"claim": "La forma más sencilla de transferir archivos entre dispositivos",
"tweet_title": "Tweetea sobre PairDrop",
"close-about_aria-label": "Cerrar Sobre PairDrop",
"buy-me-a-coffee_title": "¡Cómprame un café!",
"github_title": "PairDrop en GitHub",
"faq_title": "Preguntas frecuentes"
},
"document-titles": {
"file-transfer-requested": "Transferencia de archivos solicitada",
"image-transfer-requested": "Transferencia de imagen solicitada",
"message-received-plural": "{{count}} Mensajes recibidos",
"message-received": "Mensaje recibido",
"file-received": "Archivo Recibido",
"file-received-plural": "{{count}} Archivos Recibidos"
}
}

View File

@@ -0,0 +1,160 @@
{
"header": {
"about_title": "À propos de PairDrop",
"language-selector_title": "Choix de la langue",
"about_aria-label": "Ouvrir à propos de PairDrop",
"theme-auto_title": "Adapter le thème au système",
"theme-light_title": "Toujours utiliser le thème clair",
"theme-dark_title": "Toujours utiliser le thème sombre",
"notification_title": "Activer les notifications",
"install_title": "Installer PairDrop",
"pair-device_title": "Associez vos appareils de manière permanente",
"edit-paired-devices_title": "Gérer les appareils couplés",
"join-public-room_title": "Rejoindre temporairement la salle publique",
"cancel-paste-mode": "Terminé"
},
"instructions": {
"no-peers_data-drop-bg": "Déposer pour choisir le destinataire",
"no-peers-title": "Ouvrez PairDrop sur d'autres appareils pour envoyer des fichiers",
"no-peers-subtitle": "Associez des appareils ou entrez dans une salle publique pour être visible sur d'autres réseaux",
"x-instructions_desktop": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
"x-instructions_mobile": "Appuyez pour envoyer des fichiers ou appuyez longuement pour envoyer un message",
"x-instructions_data-drop-peer": "Déposer pour envoyer au destinataire",
"x-instructions_data-drop-bg": "Lâcher pour choisir le destinataire",
"click-to-send": "Cliquez pour envoyer",
"tap-to-send": "Appuyez pour envoyer",
"activate-paste-mode-base": "Ouvrez PairDrop sur d'autres appareils pour envoyer",
"activate-paste-mode-and-other-files": "et {{count}} autres fichiers",
"activate-paste-mode-shared-text": "texte partagé"
},
"footer": {
"known-as": "Vous êtes connu comme :",
"display-name_data-placeholder": "Chargement…",
"display-name_title": "Modifiez le nom de votre appareil de manière permanente",
"discovery": "Vous pouvez être découvert :",
"on-this-network": "sur ce réseau",
"on-this-network_title": "Vous pouvez être découvert par tout le monde sur ce réseau.",
"paired-devices": "par les appareils couplés",
"paired-devices_title": "Vous pouvez être découvert par les appareils couplés à tout moment, indépendamment du réseau.",
"public-room-devices": "dans la salle {{roomId}}",
"public-room-devices_title": "Vous pouvez être découvert par les appareils de cette salle publique indépendamment du réseau.",
"traffic": "Le trafic est",
"routed": "routé via le serveur",
"webrtc": "si WebRTC n'est pas disponible.",
"display-name_placeholder": "Chargement…"
},
"dialogs": {
"pair-devices-title": "Associer les appareils de manière permanente",
"input-key-on-this-device": "Saisissez cette clé sur un autre appareil",
"scan-qr-code": "ou scannez le QR-code.",
"enter-key-from-another-device": "Entrez ici la clé d'un autre appareil.",
"temporary-public-room-title": "Salle publique temporaire",
"input-room-id-on-another-device": "Saisissez cet ID de salle sur un autre appareil",
"enter-room-id-from-another-device": "Entrez l'ID de la salle depuis un autre appareil pour rejoindre la salle.",
"hr-or": "OU",
"pair": "associer",
"cancel": "Annuler",
"edit-paired-devices-title": "Modifier les appareils couplés",
"unpair": "Dissocier",
"paired-devices-wrapper_data-empty": "Aucun appareil couplé.",
"auto-accept-instructions-1": "Activer",
"auto-accept": "auto-accepter",
"auto-accept-instructions-2": "pour accepter automatiquement tous les fichiers envoyés depuis cet appareil.",
"close": "Fermer",
"join": "Rejoindre",
"leave": "Partir",
"would-like-to-share": "aimerait partager",
"accept": "Accepter",
"decline": "Refuser",
"has-sent": "a envoyé :",
"share": "Partage",
"download": "Télécharger",
"send-message-title": "Envoyer un message",
"send-message-to": "Envoyer un message à",
"send": "Envoyer",
"receive-text-title": "Message reçu",
"copy": "Copier",
"base64-processing": "Traitement…",
"base64-tap-to-paste": "Appuyez ici pour coller {{type}}",
"base64-paste-to-send": "Coller ici pour envoyer {{type}}",
"base64-text": "texte",
"base64-files": "fichiers",
"file-other-description-image": "et 1 autre image",
"file-other-description-file": "et 1 autre fichier",
"file-other-description-image-plural": "et {{count}} autres images",
"file-other-description-file-plural": "et {{count}} autres fichiers",
"title-image": "Image",
"title-file": "Fichier",
"title-image-plural": "Images",
"title-file-plural": "Fichiers",
"receive-title": "{{descriptor}} Reçu",
"download-again": "Télécharger à nouveau",
"language-selector-title": "Définir la langue",
"system-language": "Langue du système"
},
"about": {
"close-about_aria-label": "Fermer à propos de PairDrop",
"claim": "Le moyen le plus simple de transférer des fichiers entre appareils",
"github_title": "PairDrop sur GitHub",
"buy-me-a-coffee_title": "Acheté-moi un café!",
"tweet_title": "Tweet à propos de PairDrop",
"faq_title": "Questions fréquemment posées"
},
"notifications": {
"display-name-changed-permanently": "Le nom d'affichage est modifié de manière permanente.",
"display-name-changed-temporarily": "Le nom d'affichage est modifié uniquement pour cette session.",
"display-name-random-again": "Le nom d'affichage est à nouveau généré aléatoirement.",
"download-successful": "{{descriptor}} téléchargé",
"pairing-tabs-error": "Le couplage de deux onglets de navigateur Web est impossible.",
"pairing-success": "Appareils couplés.",
"pairing-not-persistent": "Les appareils couplés ne sont pas persistants.",
"pairing-key-invalid": "Clé invalide",
"pairing-key-invalidated": "Clé {{key}} invalidée.",
"pairing-cleared": "Tous les appareils ne sont plus appairés.",
"public-room-id-invalid": "ID de salle non valide",
"public-room-left": "Salle publique {{publicRoomId}} quittée",
"copied-to-clipboard": "Copié dans le presse-papier",
"copied-to-clipboard-error": "Copie impossible. Copier manuellement.",
"text-content-incorrect": "Le contenu du texte est incorrect.",
"file-content-incorrect": "Le contenu du fichier est incorrect.",
"clipboard-content-incorrect": "Le contenu du presse-papiers est incorrect.",
"notifications-enabled": "Notifications activées.",
"link-received": "Lien reçu par {{name}} - Cliquez pour ouvrir",
"message-received": "Message reçu par {{name}} - Cliquez pour copier",
"click-to-download": "Cliquez pour télécharger",
"request-title": "{{name}} souhaite transférer {{count}} {{descriptor}}",
"click-to-show": "Cliquez pour afficher",
"copied-text": "Texte copié dans le presse-papiers",
"copied-text-error": "L'écriture dans le presse-papiers a échoué. Copiez manuellement !",
"offline": "Vous êtes hors ligne",
"online": "Vous êtes de nouveau en ligne",
"connected": "Connecté.",
"online-requirement-pairing": "Vous devez être en ligne pour coupler des appareils.",
"online-requirement-public-room": "Vous devez être en ligne pour créer une salle publique.",
"connecting": "Connexion…",
"files-incorrect": "Les fichiers sont incorrects.",
"file-transfer-completed": "Transfert de fichier terminé.",
"ios-memory-limit": "L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois",
"message-transfer-completed": "Transfert de message terminé.",
"unfinished-transfers-warning": "Il y a des transferts inachevés. Êtes-vous sûr de vouloir fermer PairDrop ?",
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",
"selected-peer-left": "Appareils sélectionnés restants."
},
"document-titles": {
"file-received": "Fichier reçu",
"file-received-plural": "{{count}} fichiers reçus",
"file-transfer-requested": "Transfert de fichier demandé",
"image-transfer-requested": "Transfert d'image demandé",
"message-received": "Message reçu",
"message-received-plural": "{{count}} Messages reçus"
},
"peer-ui": {
"click-to-send-paste-mode": "Cliquez pour envoyer {{descriptor}}",
"click-to-send": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
"connection-hash": "Pour vérifier la sécurité du chiffrement de bout en bout, comparez ce numéro de sécurité sur les deux appareils",
"preparing": "Préparation…",
"waiting": "En attente…",
"processing": "En cours…",
"transferring": "Transfert en cours…"
}
}

View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "jika WebRTC tidak tersedia.",
"public-room-devices_title": "Anda dapat ditemukan oleh perangkat di ruang publik ini terlepas dari jaringan.",
"display-name_data-placeholder": "Memuat…",
"display-name_title": "Edit nama perangkat Anda scr. permanen",
"traffic": "Lalu lintas",
"paired-devices_title": "Anda dapat ditemukan oleh perangkat yang dipasangkan setiap saat tergantung pada jaringan.",
"public-room-devices": "dalam room {{roomId}}",
"paired-devices": "pada prngkt. yg. dipasangkan",
"on-this-network": "pada jaringan ini",
"routed": "diarahkan melalui server",
"discovery": "Anda dapat ditemukan:",
"on-this-network_title": "Anda dapat ditemukan oleh semua orang di jaringan ini.",
"known-as": "Anda dikenal sebagai:"
},
"notifications": {
"request-title": "{{name}} ingin mentransfer {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Ada transfer yang belum selesai. Apakah Anda yakin ingin menutup PairDrop?",
"message-received": "Pesan diterima dari {{name}} - Klik untuk menyalin",
"rate-limit-join-key": "Batasan tercapai. Tunggu 10 detik dan coba lagi.",
"connecting": "Menghubungkan…",
"pairing-key-invalidated": "Kunci {{key}} tidak valid.",
"pairing-key-invalid": "Kunci tidak valid",
"connected": "Tersambung.",
"pairing-not-persistent": "Perangkat dipasangkan tidak akan bertahan lama.",
"text-content-incorrect": "Isi teks keliru.",
"message-transfer-completed": "Transfer pesan selesai.",
"file-transfer-completed": "Transfer file selesai.",
"file-content-incorrect": "Isi file keliru.",
"files-incorrect": "File tidak benar.",
"selected-peer-left": "Rekan terpilih keluar.",
"link-received": "Tautan diterima dari {{name}} - Klik untuk membuka",
"online": "Anda kembali online",
"public-room-left": "Keluar dari ruang publik {{publicRoomId}}",
"copied-text": "Teks disalin ke papan klip",
"display-name-random-again": "Nama tampilan dibuat secara acak lagi.",
"display-name-changed-permanently": "Nama tampilan diubah secara permanen.",
"copied-to-clipboard-error": "Penyalinan tak dapat dilakukan. Salinlah secara manual.",
"pairing-success": "Perangkat dipasangkan.",
"clipboard-content-incorrect": "Isi papan klip keliru.",
"display-name-changed-temporarily": "Nama tampilan hanya diubah untuk sesi ini.",
"copied-to-clipboard": "Disalin ke papan klip",
"offline": "Anda sedang offline",
"pairing-tabs-error": "Memasangkan dua tab browser web tidak mungkin dilakukan.",
"public-room-id-invalid": "Room ID tidak valid",
"click-to-download": "Klik untuk mengunduh",
"pairing-cleared": "Semua Perangkat dilepaskan.",
"notifications-enabled": "Notifikasi diaktifkan.",
"online-requirement-pairing": "Anda harus online untuk memasangkan perangkat.",
"ios-memory-limit": "Mengirim file ke iOS hanya dapat dilakukan hingga 200 MB sekaligus",
"online-requirement-public-room": "Anda harus online untuk membuat ruang publik.",
"copied-text-error": "Menyalin ke papan klip gagal. Salinlah secara manual!",
"download-successful": "{{descriptor}} diunduh",
"click-to-show": "Klik untuk menampilkan",
"notifications-permissions-error": "Izin pemberitahuan telah diblokir karena pengguna telah mengabaikan permintaan izin beberapa kali. Hal ini dapat diatur ulang di Info Halaman yang dapat diakses dengan mengeklik ikon kunci di sebelah bar URL.",
"pair-url-copied-to-clipboard": "Tautan untuk memasangkan perangkat ini disalin ke papan klip",
"room-url-copied-to-clipboard": "Tautan ke ruang publik disalin ke papan klip"
},
"header": {
"cancel-paste-mode": "Selesai",
"theme-auto_title": "Sesuaikan tema dengan sistem",
"install_title": "Instal PairDrop",
"theme-dark_title": "Selalu gunakan tema gelap",
"pair-device_title": "Pasangkan perangkat anda secara permanen",
"join-public-room_title": "Bergabung dgn. ruang publik sementara",
"notification_title": "Aktifkan notifikasi",
"edit-paired-devices_title": "Edit perangkat yg. dipasangkan",
"language-selector_title": "Atur Bahasa",
"about_title": "Tentang PairDrop",
"about_aria-label": "Buka Tentang PairDrop",
"theme-light_title": "Selalu gunakan tema terang"
},
"instructions": {
"x-instructions_mobile": "Ketuk untuk mengirim file atau ketuk lama untuk mengirim pesan",
"click-to-send": "Klik untuk mengirim",
"activate-paste-mode-and-other-files": "dan {{count}} file lainnya",
"tap-to-send": "Ketuk untuk mengirim",
"activate-paste-mode-base": "Buka PairDrop di perangkat lain untuk berkirim",
"no-peers-subtitle": "Pasangkan perangkat atau masuk ke ruang publik agar dapat terdeteksi di jaringan lain",
"activate-paste-mode-shared-text": "teks bersama",
"x-instructions_desktop": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
"no-peers-title": "Buka PairDrop di perangkat lain untuk berkirim file",
"x-instructions_data-drop-peer": "Lepaskan untuk mengirim ke rekan",
"x-instructions_data-drop-bg": "Lepaskan untuk memilih penerima",
"no-peers_data-drop-bg": "Lepaskan untuk memilih penerima"
},
"peer-ui": {
"processing": "Memproses…",
"click-to-send-paste-mode": "Klik untuk mengirim {{descriptor}}",
"click-to-send": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
"waiting": "Menunggu…",
"connection-hash": "Untuk memverifikasi keamanan enkripsi end-to-end, bandingkan nomor keamanan ini pada kedua perangkat",
"preparing": "Menyiapkan…",
"transferring": "Mentransfer…"
},
"dialogs": {
"base64-paste-to-send": "Tempel di sini untuk mengirim {{type}}",
"auto-accept-instructions-2": "untuk secara otomatis menerima semua file yang dikirim dari perangkat tersebut.",
"receive-text-title": "Pesan Diterima",
"edit-paired-devices-title": "Edit Perangkat yg. Dipasangkan",
"cancel": "Batal",
"auto-accept-instructions-1": "Aktifkan",
"pair-devices-title": "Pasangkan Perangkat Scr. Permanen",
"download": "Unduh",
"title-file": "File",
"base64-processing": "Memproses…",
"decline": "Tolak",
"receive-title": "{{descriptor}} Diterima",
"leave": "Tinggalkan",
"join": "Gabung",
"title-image-plural": "Gambar",
"send": "Kirim",
"base64-tap-to-paste": "Ketuk di sini untuk menempelkan {{type}}",
"base64-text": "teks",
"copy": "Salin",
"file-other-description-image": "dan 1 gambar lainnya",
"temporary-public-room-title": "Ruang Publik Sementara",
"base64-files": "file",
"has-sent": "telah mengirim:",
"file-other-description-file": "dan 1 file lainnya",
"close": "Tutup",
"system-language": "Bahasa Sistem",
"unpair": "Lepas",
"title-image": "Gambar",
"file-other-description-file-plural": "dan {{count}} file lainnya",
"would-like-to-share": "ingin berbagi",
"send-message-to": "Kirim pesan ke",
"language-selector-title": "Pilih Bahasa",
"pair": "Pasangkan",
"hr-or": "ATAU",
"scan-qr-code": "atau pindai kode QR.",
"input-key-on-this-device": "Masukkan kunci ini pada perangkat lain",
"download-again": "Unduh lagi",
"accept": "Terima",
"paired-devices-wrapper_data-empty": "Tak ada perangkat yg. dipasangkan.",
"enter-key-from-another-device": "Masukkan kunci dari perangkat lain di sini.",
"share": "Bagikan",
"auto-accept": "terima-otomatis",
"title-file-plural": "File",
"send-message-title": "Kirim Pesan",
"input-room-id-on-another-device": "Masukkan room ID ini pada perangkat lain",
"file-other-description-image-plural": "dan {{count}} gambar lainnya",
"enter-room-id-from-another-device": "Masukkan room ID dari perangkat lain untuk bergabung dengan room.",
"message_title": "Masukkan pesan untuk dikirim",
"pair-devices-qr-code_title": "Klik untuk menyalin tautan untuk memasangkan perangkat ini",
"public-room-qr-code_title": "Klik untuk menyalin tautan ke ruang publik"
},
"about": {
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
"tweet_title": "Tweet tentang PairDrop",
"close-about_aria-label": "Tutup Tentang PairDrop",
"buy-me-a-coffee_title": "Traktir aku kopi!",
"github_title": "PairDrop di GitHub",
"faq_title": "Pertanyaan yang sering diajukan"
},
"document-titles": {
"file-transfer-requested": "Permintaan Transfer File",
"message-received-plural": "{{count}} Pesan Diterima",
"message-received": "Pesan Diterima",
"file-received": "File Diterima",
"file-received-plural": "{{count}} File Diterima",
"image-transfer-requested": "Permintaan Transfer Gambar"
}
}

View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "se WebRTC non è disponibile.",
"public-room-devices_title": "Puoi essere rilevato dai dispositivi presenti in questa stanza pubblica indipendentemente dalla rete.",
"display-name_data-placeholder": "Caricamento…",
"display-name_title": "Modifica il nome del tuo dispositivo permanentemente",
"traffic": "Il traffico è",
"paired-devices_title": "Puoi essere rilevato dai dispositivi abbinati in ogni momento, indipendentemente dalla rete.",
"public-room-devices": "nella stanza {{roomId}}",
"paired-devices": "da dispositivi abbinati",
"on-this-network": "su questa rete",
"routed": "instradato attraverso il server",
"discovery": "Puoi essere rilevato:",
"on-this-network_title": "puoi essere rilevato da chiunque su questa rete.",
"known-as": "Sei visibile come:"
},
"header": {
"cancel-paste-mode": "Fatto",
"theme-auto_title": "Adatta il tema al sistema automaticamente",
"install_title": "Installa PairDrop",
"theme-dark_title": "Usa sempre il tema scuro",
"pair-device_title": "Abbina i tuoi dispositivi permanentemente",
"join-public-room_title": "Unisciti ad una stanza pubblica temporaneamente",
"notification_title": "Attiva notifiche",
"edit-paired-devices_title": "Modifica i dispositivi abbinati",
"language-selector_title": "Imposta Lingua",
"about_title": "Informazioni su PairDrop",
"about_aria-label": "Apri Informazioni su PairDrop",
"theme-light_title": "Usa sempre il tema chiaro"
},
"instructions": {
"x-instructions_mobile": "Tocca per inviare file o tocco prolungato per inviare un messaggio",
"click-to-send": "Clicca per inviare",
"activate-paste-mode-and-other-files": "e altri {{count}} files",
"tap-to-send": "Tocca per inviare",
"activate-paste-mode-base": "Apri PairDrop su altri dispositivi per inviare",
"no-peers-subtitle": "Abbina dispositivi o entra in una stanza pubblica per essere rilevabile su altre reti",
"activate-paste-mode-shared-text": "testo condiviso",
"x-instructions_desktop": "Clicca per inviare files o usa il tasto destro per inviare un messaggio",
"no-peers-title": "Apri PairDrop su altri dispositivi per inviare files",
"x-instructions_data-drop-peer": "Rilascia per inviare al peer",
"x-instructions_data-drop-bg": "Rilascia per selezionare il destinatario",
"no-peers_data-drop-bg": "Rilascia per selezionare il destinatario"
},
"dialogs": {
"auto-accept-instructions-2": "per accettare automaticamente tutti i files inviati da quel dispositivo.",
"edit-paired-devices-title": "Modifica Dispositivi Abbinati",
"cancel": "Annulla",
"auto-accept-instructions-1": "Attiva",
"pair-devices-title": "Abbina Dispositivi Permanentemente",
"temporary-public-room-title": "Stanza Pubblica Temporanea",
"close": "Chiudi",
"unpair": "Dissocia",
"pair": "Abbina",
"scan-qr-code": "o scannerizza il codice QR.",
"input-key-on-this-device": "Inserisci questo codice su un altro dispositivo",
"paired-devices-wrapper_data-empty": "Nessun dispositivo abbinato.",
"enter-key-from-another-device": "Inserisci il codice dell'altro dispositivo qui.",
"auto-accept": "accetta-automaticamente",
"input-room-id-on-another-device": "Inserisci l'ID di questa stanza su un altro dispositivo",
"enter-room-id-from-another-device": "Inserisci l'ID stanza da un altro dispositivo per accedere alla stanza.",
"base64-paste-to-send": "Incolla qui per inviare {{type}}",
"receive-text-title": "Messaggio Ricevuto",
"download": "Scarica",
"title-file": "File",
"base64-processing": "Elaborazione…",
"decline": "Rifiuta",
"receive-title": "{{descriptor}} Ricevuto",
"leave": "Abbandona",
"join": "Unisciti",
"title-image-plural": "Immagini",
"send": "Invia",
"base64-tap-to-paste": "Tocca qui per incollare {{type}}",
"base64-text": "testo",
"copy": "Copia",
"file-other-description-image": "e 1 altra immagine",
"base64-files": "files",
"has-sent": "ha inviato:",
"file-other-description-file": "ed 1 altro file",
"system-language": "Lingua di Sistema",
"title-image": "Immagine",
"file-other-description-file-plural": "e altri {{count}} files",
"would-like-to-share": "vorrebbe condividere",
"send-message-to": "Invia un messaggio a",
"language-selector-title": "Imposta Lingua",
"hr-or": "OPPURE",
"download-again": "Scarica ancora",
"accept": "Accetta",
"share": "Condividi",
"title-file-plural": "Files",
"send-message-title": "Invia Messaggio",
"file-other-description-image-plural": "e {{count}} altre immagini",
"message_title": "Inserisci il messaggio da inviare",
"pair-devices-qr-code_title": "Clicca per copiare il link di abbinamento di questo dispositivo",
"public-room-qr-code_title": "Clicca per copirare il link della stanza pubblica"
},
"notifications": {
"request-title": "{{name}} vorrebbe trasferire {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Ci sono dei trasferimenti in corso. Sei sicuro di voler chiudere PairDrop?",
"message-received": "Messaggio ricevuto da {{name}} - Clicca per copiare",
"rate-limit-join-key": "Limite raggiunto. Aspetta 10 secondi e riprova.",
"connecting": "Connessione…",
"pairing-key-invalidated": "Il codice {{key}} è stato invalidato.",
"pairing-key-invalid": "Codice non valido",
"connected": "Connesso.",
"pairing-not-persistent": "I dispositivi abbinati non sono persistenti.",
"text-content-incorrect": "Il contenuto testuale non è corretto.",
"message-transfer-completed": "Trasferimento del messaggio completato.",
"file-transfer-completed": "Trasferimento file completato.",
"file-content-incorrect": "Il contenuto del file non è corretto.",
"files-incorrect": "I file non sono corretti.",
"selected-peer-left": "Peer selezionato ha abbandonato.",
"link-received": "Link ricevuto da {{name}} - Clicca per aprire",
"online": "Sei di nuovo online",
"public-room-left": "Ha lasciato la stanza pubblica {{publicRoomId}}",
"copied-text": "Testo copiato negli appunti",
"display-name-random-again": "Il nome visualizzato è generato casualmente un'altra volta.",
"display-name-changed-permanently": "Il nome visualizzato è cambiato permanentemente.",
"copied-to-clipboard-error": "La copia non è possibile. Copia manualmente.",
"pairing-success": "Dispositivi abbinati.",
"clipboard-content-incorrect": "Il contenuto copiato non è corretto.",
"display-name-changed-temporarily": "Il nome visualizzato è cambiato solo per questa sessione.",
"copied-to-clipboard": "Copiato negli appunti",
"offline": "Sei offline",
"pairing-tabs-error": "Abbinare due schede del browser è impossibile.",
"public-room-id-invalid": "ID stanza non valido",
"click-to-download": "Clicca per scaricare",
"pairing-cleared": "Tutti i dispositivi sono stati dissociati.",
"notifications-enabled": "Notifiche attivate.",
"online-requirement-pairing": "Devi essere online per abbinare 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",
"click-to-show": "Clicca per mostrare",
"notifications-permissions-error": "Il permesso all'invio delle notifiche è stato negato poiché l'utente ha ignorato varie volte le richieste di permesso. Ciò può essere ripristinato nelle \"informazioni sito\" cliccando sull'icona a forma di lucchetto vicino alla barra degli indirizzi.",
"pair-url-copied-to-clipboard": "Link di abbinamento copiato negli appunti",
"room-url-copied-to-clipboard": "Link della stanza copiato negli appunti"
},
"peer-ui": {
"processing": "Elaborazione…",
"click-to-send-paste-mode": "Clicca per inviare {{descriptor}}",
"click-to-send": "Clicca per inviare files o tasto destro per inviare un messaggio",
"waiting": "In attesa…",
"connection-hash": "Per verificare la sicurezza della crittografia end-to-end, confronta questo numero di sicurezza su entrambi i dispositivi",
"preparing": "Preparazione…",
"transferring": "Trasferimento…"
},
"about": {
"claim": "Il modo più semplice per trasferire files tra dispositivi",
"tweet_title": "Twitta riguardo PairDrop",
"close-about_aria-label": "Chiudi Informazioni su PairDrop",
"buy-me-a-coffee_title": "Comprami un caffè!",
"github_title": "PairDrop su GitHub",
"faq_title": "Domande Frequenti"
},
"document-titles": {
"file-transfer-requested": "Trasferimento File Richiesto",
"image-transfer-requested": "Trasferimento Immagine Richiesto",
"message-received-plural": "{{count}} Messaggi ricevuti",
"message-received": "Messaggio ricevuto",
"file-received": "File Ricevuto",
"file-received-plural": "{{count}} Files Ricevuti"
}
}

View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "WebRTCが利用できない場合。",
"public-room-devices_title": "このデバイスはネットワークと関係なく、このパブリックルームのデバイスにより発見される可能性があります。",
"display-name_data-placeholder": "読み込み中…",
"display-name_title": "永続的なデバイス名を編集する",
"traffic": "トラフィックは",
"paired-devices_title": "このデバイスはネットワークと関係なく、常にペア設定したデバイスにより発見される可能性があります。",
"public-room-devices": "ルーム{{roomId}}上",
"paired-devices": "ペア設定したデバイス",
"on-this-network": "このネットワーク上",
"routed": "サーバーを経由します",
"discovery": "このデバイスは以下で発見される可能性があります:",
"on-this-network_title": "このデバイスはこのネットワーク上の誰にでも発見される可能性があります。",
"known-as": "他のデバイスに表示される名前:"
},
"notifications": {
"request-title": "{{name}}は{{count}}個の{{descriptor}}を共有しようとしています",
"unfinished-transfers-warning": "未完了の転送があります。本当にPairDropを終了しますか",
"message-received": "{{name}}から受信したメッセージ(クリックしてコピー)",
"rate-limit-join-key": "レート制限に到達しました。10秒待ってから再度お試しください。",
"connecting": "接続中…",
"pairing-key-invalidated": "コード{{key}}が失効しました。",
"pairing-key-invalid": "無効なコード",
"connected": "接続しました。",
"pairing-not-persistent": "ペア設定されたデバイスは永続化されていません。",
"text-content-incorrect": "テキストの内容が不正です。",
"message-transfer-completed": "メッセージの送信が完了しました。",
"file-transfer-completed": "ファイルの転送が完了しました。",
"file-content-incorrect": "ファイルの内容が不正です。",
"files-incorrect": "ファイルが間違っています。",
"selected-peer-left": "選択した相手が退出しました。",
"link-received": "{{name}}から受信したリンク(クリックして開く)",
"online": "オンラインに復帰しました",
"public-room-left": "パブリックルーム{{publicRoomId}}から退出しました",
"copied-text": "テキストをクリップボードにコピーしました",
"display-name-random-again": "表示名がもう一度ランダムに生成されました。",
"display-name-changed-permanently": "永続的な表示名が変更されました。",
"copied-to-clipboard-error": "コピーできませんでした。手動でコピーしてください。",
"pairing-success": "デバイスがペア設定されました。",
"clipboard-content-incorrect": "クリップボードの内容が不正です。",
"display-name-changed-temporarily": "このセッションでの表示名が変更されました。",
"copied-to-clipboard": "クリップボードにコピーしました",
"offline": "オフラインです",
"pairing-tabs-error": "同じWebブラウザーの2つのタブをペア設定することはできません。",
"public-room-id-invalid": "無効なルームID",
"click-to-download": "クリックしてダウンロード",
"pairing-cleared": "全てのデバイスのペア設定を解除しました。",
"notifications-enabled": "通知が有効です。",
"online-requirement-pairing": "デバイスをペア設定するにはオンラインである必要があります。",
"ios-memory-limit": "iOSへのファイル送信は一度に200MBまでしかできません",
"online-requirement-public-room": "パブリックルームを作成するにはオンラインである必要があります。",
"copied-text-error": "クリップボードにコピーできませんでした。手動でコピーしてください。",
"download-successful": "{{descriptor}}をダウンロードしました",
"click-to-show": "クリックして表示",
"notifications-permissions-error": "ユーザーが権限のプロンプトを何度か閉じたため、通知の権限がブロックされました。これは、URL バーの横にある鍵アイコンをクリックしてアクセスできるページ情報からリセットできます。",
"pair-url-copied-to-clipboard": "このデバイスをペア設定するリンクをクリップボードにコピーしました",
"room-url-copied-to-clipboard": "パブリックルームへのリンクをクリップボードにコピーしました"
},
"header": {
"cancel-paste-mode": "完了",
"theme-auto_title": "テーマをシステムの設定に自動的に合わせる",
"install_title": "PairDropをインストール",
"theme-dark_title": "常にダークテーマを使用する",
"pair-device_title": "あなたのデバイスを永続的にペア設定する",
"join-public-room_title": "パブリックルームに一時的に参加する",
"notification_title": "通知を有効にする",
"edit-paired-devices_title": "ペア設定したデバイスを編集する",
"language-selector_title": "言語を設定",
"about_title": "PairDropについて",
"about_aria-label": "PairDropについてを開く",
"theme-light_title": "常にライトテーマを使用する"
},
"instructions": {
"x-instructions_mobile": "タップしてファイルを送信または長押ししてメッセージを送信します",
"click-to-send": "クリックして送信",
"activate-paste-mode-and-other-files": "とその他{{count}}個のファイル",
"tap-to-send": "タップして送信",
"activate-paste-mode-base": "他のデバイスでPairDropを開いて送信します",
"no-peers-subtitle": "デバイスをペア設定するかパブリックルームに参加すると、他のネットワーク上からも見つけられるようになります",
"activate-paste-mode-shared-text": "共有されたテキスト",
"x-instructions_desktop": "左クリックしてファイルを送信または右クリックしてメッセージを送信します",
"no-peers-title": "他のデバイスでPairDropを開いてファイルを送信します",
"x-instructions_data-drop-peer": "離すとこの相手に送信します",
"x-instructions_data-drop-bg": "送信したい相手の上で離してください",
"no-peers_data-drop-bg": "送信したい相手の上で離してください"
},
"peer-ui": {
"processing": "処理中…",
"click-to-send-paste-mode": "クリックして{{descriptor}}を送信",
"click-to-send": "クリックしてファイルを送信または右クリックしてメッセージを送信します",
"waiting": "待機中…",
"connection-hash": "エンドツーエンド暗号化のセキュリティを確認するには、両方のデバイスのセキュリティナンバーを確認します",
"preparing": "準備中…",
"transferring": "転送中…"
},
"dialogs": {
"base64-paste-to-send": "ここをタップして{{type}}を送信",
"auto-accept-instructions-2": "そのデバイスから送信される全てのファイルを自動的に承諾します。",
"receive-text-title": "メッセージを受信しました",
"edit-paired-devices-title": "ペア設定したデバイスを編集",
"cancel": "キャンセル",
"auto-accept-instructions-1": "有効化",
"pair-devices-title": "デバイスを永続的にペア設定",
"download": "ダウンロード",
"title-file": "ファイル",
"base64-processing": "処理中…",
"decline": "拒否",
"receive-title": "{{descriptor}}を受信しました",
"leave": "退出",
"join": "参加",
"title-image-plural": "複数の画像",
"send": "送信",
"base64-tap-to-paste": "ここをタップして{{type}}を貼り付け",
"base64-text": "テキスト",
"copy": "コピー",
"file-other-description-image": "と1個の他の画像",
"temporary-public-room-title": "一時的なパブリックルーム",
"base64-files": "ファイル",
"has-sent": "が送信しました:",
"file-other-description-file": "と1個の他のファイル",
"close": "閉じる",
"system-language": "システムの言語",
"unpair": "ペア解除",
"title-image": "画像",
"file-other-description-file-plural": "と{{count}}個の他のファイル",
"would-like-to-share": "が以下のファイルを共有しようとしています",
"send-message-to": "このデバイスにメッセージを送信:",
"language-selector-title": "言語を設定",
"pair": "ペア設定",
"hr-or": "または",
"scan-qr-code": "もしくはQRコードをスキャンします。",
"input-key-on-this-device": "このキーを他のデバイスに入力する",
"download-again": "もう一度ダウンロードする",
"accept": "承諾",
"paired-devices-wrapper_data-empty": "ペア設定したデバイスはありません。",
"enter-key-from-another-device": "他のデバイスに表示されたキーをここに入力します。",
"share": "共有",
"auto-accept": "自動承諾",
"title-file-plural": "複数のファイル",
"send-message-title": "メッセージを送信",
"input-room-id-on-another-device": "このルームIDを他のデバイスに入力",
"file-other-description-image-plural": "と{{count}}個の他の画像",
"enter-room-id-from-another-device": "他のデバイスに表示された参加したいルームのIDを入力します。",
"message_title": "送信するメッセージを挿入",
"pair-devices-qr-code_title": "クリックしてこのデバイスをペア設定するリンクをコピー",
"public-room-qr-code_title": "クリックしてパブリックルームへのリンクをコピー"
},
"about": {
"claim": "デバイス間でファイルを転送する最も簡単な方法",
"tweet_title": "PairDropについてツイートする",
"close-about_aria-label": "PairDropについてを閉じる",
"buy-me-a-coffee_title": "コーヒーをおごってください!",
"github_title": "GitHubでPairDropを見る",
"faq_title": "FAQ"
},
"document-titles": {
"file-transfer-requested": "ファイルの転送がリクエストされました",
"image-transfer-requested": "画像の転送がリクエストされました",
"message-received-plural": "{{count}}個のメッセージを受信しました",
"message-received": "メッセージを受信しました",
"file-received": "ファイルを受信しました",
"file-received-plural": "{{count}}個のファイルを受信しました"
}
}

View File

@@ -0,0 +1,138 @@
{
"header": {
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
"about_title": "Om PairDrop",
"about_aria-label": "Åpne «Om PairDrop»",
"theme-auto_title": "Juster drakt til system",
"theme-light_title": "Alltid bruk lys drakt",
"theme-dark_title": "Alltid bruk mørk drakt",
"notification_title": "Skru på merknader",
"cancel-paste-mode": "Ferdig",
"install_title": "Installer PairDrop",
"pair-device_title": "Sammenkoble enhet"
},
"footer": {
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
"display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
"known-as": "Du er kjent som:",
"paired-devices": "sammenkoblede enheter",
"routed": "Sendes gjennom tjeneren"
},
"instructions": {
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
"click-to-send": "Klikk for å sende",
"no-peers_data-drop-bg": "Slipp for å velge mottager",
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
"tap-to-send": "Trykk for å sende",
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
"activate-paste-mode-shared-text": "delt tekst"
},
"dialogs": {
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
"pair-devices-title": "Sammenkoble enheter",
"would-like-to-share": "ønsker å dele",
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter.",
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
"accept": "Godta",
"has-sent": "har sendt:",
"base64-paste-to-send": "Trykk her for å sende {{type}}",
"base64-text": "tekst",
"base64-files": "filer",
"file-other-description-image-plural": "og {{count}} andre bilder",
"receive-title": "{{descriptor}} mottatt",
"send-message-title": "Send melding",
"base64-processing": "Behandler…",
"close": "Lukk",
"decline": "Avslå",
"download": "Last ned",
"copy": "Kopier",
"pair": "Sammenkoble",
"cancel": "Avbryt",
"scan-qr-code": "eller skann QR-koden.",
"auto-accept-instructions-1": "Aktiver",
"receive-text-title": "Melding mottatt",
"auto-accept": "auto-godkjenn",
"share": "Del",
"send-message-to": "Send en melding til",
"send": "Send",
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
"file-other-description-image": "og ett annet bilde",
"file-other-description-file-plural": "og {{count}} andre filer",
"title-file-plural": "Filer",
"download-again": "Last ned igjen",
"file-other-description-file": "og én annen fil",
"title-image": "Bilde",
"title-file": "Fil",
"title-image-plural": "Bilder"
},
"about": {
"close-about_aria-label": "Lukk «Om PairDrop»",
"faq_title": "Ofte stilte spørsmål",
"claim": "Den enkleste måten å overføre filer mellom enheter",
"buy-me-a-coffee_title": "Spander drikke.",
"tweet_title": "Tvitre om PairDrop",
"github_title": "PairDrop på GitHub"
},
"notifications": {
"copied-to-clipboard": "Kopiert til utklippstavlen",
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
"notifications-enabled": "Merknader påskrudd.",
"click-to-show": "Klikk for å vise",
"copied-text": "Tekst kopiert til utklippstavlen",
"connected": "Tilkoblet.",
"online": "Du er tilbake på nett",
"file-transfer-completed": "Filoverføring utført.",
"selected-peer-left": "Valgt likemann dro.",
"pairing-key-invalid": "Ugyldig nøkkel",
"connecting": "Kobler til …",
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
"offline": "Du er frakoblet",
"online-requirement": "Du må være på nett for å koble sammen enheter.",
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
"text-content-incorrect": "Tekstinnholdet er uriktig.",
"file-content-incorrect": "Filinnholdet er uriktig.",
"click-to-download": "Klikk for å laste ned",
"message-transfer-completed": "Meldingsoverføring utført.",
"download-successful": "{{descriptor}} nedlastet",
"pairing-success": "Enheter sammenkoblet.",
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne",
"files-incorrect": "Filene er uriktige.",
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
},
"document-titles": {
"file-received": "Fil mottatt",
"file-received-plural": "{{count}} filer mottatt",
"message-received": "Melding mottatt",
"file-transfer-requested": "Filoverføring forespurt",
"message-received-plural": "{{count}} meldinger mottatt"
},
"peer-ui": {
"preparing": "Forbereder …",
"waiting": "Venter…",
"processing": "Behandler …",
"transferring": "Overfører …",
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
}
}

View File

@@ -0,0 +1,159 @@
{
"footer": {
"webrtc": "als WebRTC niet beschikbaar is.",
"public-room-devices_title": "U kan door apparaten gevonden worden in deze openbare ruimte, ongeacht van het netwerk.",
"display-name_data-placeholder": "Laden…",
"display-name_title": "Bewerk uw apparaatnaam permanent",
"traffic": "Dataverkeer is",
"paired-devices_title": "U kan gevonden worden door gekoppelde apparaten, ongeacht van het netwerk.",
"public-room-devices": "in kamer {{roomId}}",
"paired-devices": "door gekoppelde apparaten",
"on-this-network": "op dit netwerk",
"routed": "door de server geleid",
"discovery": "U bent zichtbaar:",
"on-this-network_title": "U kan door iedereen gevonden worden op dit netwerk.",
"known-as": "U staat bekend als:"
},
"notifications": {
"request-title": "{{name}} zou graag {{count}}{{descriptor}} overdragen",
"unfinished-transfers-warning": "Nog niet alle overdrachten zijn compleet. Weet u zeker dat u PairDrop sluiten?",
"message-received": "Bericht ontvangen van {{name}} - Klik om te kopiëren",
"rate-limit-join-key": "Tempolimiet bereikt. Wacht 10 seconde en probeer opnieuw.",
"connecting": "Verbinden…",
"pairing-key-invalidated": "Sleutel {{key}} ongeldig.",
"pairing-key-invalid": "Ongeldige sleutel",
"connected": "Verbonden.",
"pairing-not-persistent": "Gekoppelde apparaten zijn niet persistent.",
"text-content-incorrect": "Tekst inhoud is incorrect.",
"message-transfer-completed": "Berichtsoverdracht compleet.",
"file-transfer-completed": "Bestandsoverdracht compleet.",
"file-content-incorrect": "Bestandsinhoud is incorrect.",
"files-incorrect": "Bestanden zijn incorrect.",
"selected-peer-left": "Gekozen peer is vertrokken.",
"link-received": "Link van {{name}} ontvangen - Klik om te openen",
"online": "U bent terug online",
"public-room-left": "Openbare ruimte {{publicRoomId}} verlaten",
"copied-text": "Tekst naar klembord gekopieërd",
"display-name-random-again": "De weergavenaam is opnieuw willekeurig gegenereerd.",
"display-name-changed-permanently": "De weergavenaam is permanent gewijzigd.",
"copied-to-clipboard-error": "Kopiëren is niet mogelijk. Kopieer handmatig.",
"pairing-success": "Apparaten gekoppeld.",
"clipboard-content-incorrect": "De inhoud van het klembord is incorrect.",
"display-name-changed-temporarily": "De weergavenaam is alleen voor deze sessie gewijzigd.",
"copied-to-clipboard": "Gekopieerd naar klembord",
"offline": "U bent offline",
"pairing-tabs-error": "Twee webbrowser tabbladen koppelen in is onmogelijk.",
"public-room-id-invalid": "Ongeldig kamer ID",
"click-to-download": "Klik om te downloaden",
"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",
"click-to-show": "Klik om te tonen"
},
"header": {
"cancel-paste-mode": "Klaar",
"theme-auto_title": "Gebruik systeemstijl",
"install_title": "PairDrop installeren",
"theme-dark_title": "Altijd donkere modus gebruiken",
"pair-device_title": "Koppel uw apparaten permanent",
"join-public-room_title": "Openbare ruimte tijdelijk betreden",
"notification_title": "Meldingen inschakelen",
"edit-paired-devices_title": "Gekoppelde apparaten bewerken",
"language-selector_title": "Taal Selecteren",
"about_title": "Over PairDrop",
"about_aria-label": "Open Over PairDrop",
"theme-light_title": "Altijd lichte modus gebruiken"
},
"instructions": {
"x-instructions_mobile": "Tik om bestanden te versturen of houdt vast om een bericht te sturen",
"click-to-send": "Klik om te verzenden",
"activate-paste-mode-and-other-files": "en {{count}} andere bestanden",
"tap-to-send": "Tik om te verzenden",
"activate-paste-mode-base": "Open PairDrop op andere apparaten om te verzenden",
"no-peers-subtitle": "Koppel apparaten of betreed een openbare ruimte om op andere netwerken zichtbaar te worden",
"activate-paste-mode-shared-text": "gedeelde tekst",
"x-instructions_desktop": "Klik om bestanden te versturen of rechtsklik om een bericht te sturen",
"no-peers-title": "Open PairDrop op andere apparaten om bestanden te versturen",
"x-instructions_data-drop-peer": "Laat los om naar peer te versturen",
"x-instructions_data-drop-bg": "Loslaten om ontvanger te selecteren",
"no-peers_data-drop-bg": "Loslaten om ontvanger te kiezen"
},
"peer-ui": {
"processing": "Verwerken…",
"click-to-send-paste-mode": "Klik om {{descriptor}} te versturen",
"click-to-send": "Klik om bestanden te versturen of rechtsklik om een bericht te versturen",
"waiting": "Wachten…",
"connection-hash": "Vergelijk dit veiligheidsnummer op beide apparaten, om de beveiliging van de eind-tot-eind versleuteling te verifiëren",
"preparing": "Voorbereiden…",
"transferring": "Overdragen…"
},
"dialogs": {
"base64-paste-to-send": "Plak hier om {{type}} te versturen",
"auto-accept-instructions-2": "om automatisch alle bestanden van dat apparaat te accepteren.",
"receive-text-title": "Bericht Ontvangen",
"edit-paired-devices-title": "Gekoppelde Apparaten Bewerken",
"cancel": "Annuleer",
"auto-accept-instructions-1": "Activeer",
"pair-devices-title": "Koppel Apparaten Permanent",
"download": "Download",
"title-file": "Bestand",
"base64-processing": "Verwerken…",
"decline": "Afwijzen",
"receive-title": "{{descriptor}} Ontvangen",
"leave": "Verlaten",
"join": "Betreden",
"title-image-plural": "Afbeeldingen",
"send": "Verzenden",
"base64-tap-to-paste": "Hier tikken om {{type}} te plakken",
"base64-text": "tekst",
"copy": "Kopiëren",
"file-other-description-image": "en één andere afbeelding",
"temporary-public-room-title": "Tijdelijke Openbare Ruimte",
"base64-files": "bestanden",
"has-sent": "stuurde het volgende:",
"file-other-description-file": "en één ander bestand",
"close": "Sluiten",
"system-language": "Systeemtaal",
"unpair": "Ontkoppel",
"title-image": "Afbeelding",
"file-other-description-file-plural": "en {{count}} andere bestanden",
"would-like-to-share": "zou graag het volgende willen delen",
"send-message-to": "Verstuur een bericht naar",
"language-selector-title": "Taal Instellen",
"pair": "Koppel",
"hr-or": "OF",
"scan-qr-code": "of scan de QR-code.",
"input-key-on-this-device": "Voer deze sleutel in op een ander apparaat",
"download-again": "Download opnieuw",
"accept": "Accepteren",
"paired-devices-wrapper_data-empty": "Geen gekoppelde apparaten.",
"enter-key-from-another-device": "Voer hier de sleutel van een ander apparaat in.",
"share": "Delen",
"auto-accept": "automatisch-accepteren",
"title-file-plural": "Bestanden",
"send-message-title": "Bericht Sturen",
"input-room-id-on-another-device": "Voer de kamer ID in op een ander apparaat",
"file-other-description-image-plural": "en {{count}} andere afbeeldingen",
"enter-room-id-from-another-device": "Voer de kamer ID van een ander apparaat hier in."
},
"about": {
"claim": "De makkelijkste manier om bestanden tussen apparaten te versturen",
"tweet_title": "Tweet over PairDrop",
"close-about_aria-label": "Sluit Over PairDrop",
"buy-me-a-coffee_title": "Koop een kopje koffie voor mij!",
"github_title": "PairDrop op GitHub",
"faq_title": "Veel gestelde vragen"
},
"document-titles": {
"file-transfer-requested": "Bestandsoverdracht verzocht",
"image-transfer-requested": "Afbeeldingsoverdracht verzocht",
"message-received-plural": "{{count}} berichten ontvangen",
"message-received": "Bericht ontvangen",
"file-received": "Bestand ontvangen",
"file-received-plural": "{{count}} bestanden ontvangen"
}
}

View File

@@ -0,0 +1,165 @@
{
"footer": {
"webrtc": "dacă WebRTC nu este disponibil.",
"public-room-devices_title": "Poți fi descoperit de dispozitivele din această cameră publică, independent de rețea.",
"display-name_data-placeholder": "Se încarcă…",
"display-name_title": "Editați permanent numele dispozitivului tău",
"traffic": "Traficul este",
"paired-devices_title": "Poți fi descoperit în orice moment de dispozitivele cuplate, indiferent de rețea.",
"public-room-devices": "în camera {{roomId}}",
"paired-devices": "prin dispozitive împerecheate",
"on-this-network": "în această rețea",
"routed": "rutate prin server",
"discovery": "Poți fi descoperit:",
"on-this-network_title": "Poți fi descoperit de toată lumea din această rețea.",
"known-as": "Ești cunoscut ca:"
},
"notifications": {
"request-title": "{{name}} ar dori să transfere {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Există transferuri neterminate. Sigur vrei să închizi PairDrop?",
"message-received": "Mesaj primit de {{name}} - Apasă pentru a copia",
"rate-limit-join-key": "A fost atinsă limita ratei. Așteptați 10 secunde și încercați din nou.",
"connecting": "Conectarea…",
"pairing-key-invalidated": "Cheia {{key}} invalidată.",
"pairing-key-invalid": "Cheie invalidă",
"connected": "Conectat.",
"pairing-not-persistent": "Dispozitivele cuplate nu sunt persistente.",
"text-content-incorrect": "Conținutul textului este incorect.",
"message-transfer-completed": "Transferul mesajului este finalizat.",
"file-transfer-completed": "Transfer de fișiere finalizat.",
"file-content-incorrect": "Conținutul fișierului este incorect.",
"files-incorrect": "Fișierele sunt incorecte.",
"selected-peer-left": "Selectat peer a plecat.",
"link-received": "Link primit de {{name}} - Apasă pentru a deschide",
"online": "Ați revenit online",
"public-room-left": "Plecat din camera publică {{publicRoomId}}",
"copied-text": "Text copiat în clipboard",
"display-name-random-again": "Numele afișat este din nou generat aleatoriu.",
"display-name-changed-permanently": "Numele afișat este schimbat permanent.",
"copied-to-clipboard-error": "Copierea nu este posibilă. Copiați manual.",
"pairing-success": "Dispozitive asociate.",
"clipboard-content-incorrect": "Conținutul clipboard-ului este incorect.",
"display-name-changed-temporarily": "Numele afișat se modifică numai pentru această sesiune.",
"copied-to-clipboard": "Copiat în clipboard",
"offline": "Ești offline",
"pairing-tabs-error": "Cuplarea între două file de browser web este imposibilă.",
"public-room-id-invalid": "ID-ul camerei invalid",
"click-to-download": "Apasă pentru a descărca",
"pairing-cleared": "Toate dispozitivele sunt decuplate.",
"notifications-enabled": "Notificări activate.",
"online-requirement-pairing": "Trebuie să fiți online pentru a asocia dispozitivele.",
"ios-memory-limit": "Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan",
"online-requirement-public-room": "Trebuie să fiți online pentru a crea o cameră publică.",
"copied-text-error": "Scrierea în clipboard a eșuat. Copiați manual!",
"download-successful": "{{descriptor}} descărcat",
"click-to-show": "Apasă pentru a arăta",
"notifications-permissions-error": "Permisiunea de notificare a fost blocată deoarece utilizatorul a respins de mai multe ori solicitarea de permisiune. Acest lucru poate fi resetat în Info pagină, care poate fi accesat făcând clic pe pictograma de blocare de lângă bara URL.",
"pair-url-copied-to-clipboard": "Link pentru a asocia acest dispozitiv copiat în clipboard",
"room-url-copied-to-clipboard": "Link către sala publică copiat în clipboard"
},
"header": {
"cancel-paste-mode": "Gata",
"theme-auto_title": "Adaptează tema la sistem",
"install_title": "Instalează PairDrop",
"theme-dark_title": "Utilizați mereu tema întunecoasă",
"pair-device_title": "Împerechează-ți permanent dispozitivele",
"join-public-room_title": "Alătură-te temporar camerei publice",
"notification_title": "Activați notificări",
"edit-paired-devices_title": "Editați dispozitivele împerecheate",
"language-selector_title": "Setează Limba",
"about_title": "Despre PairDrop",
"about_aria-label": "Deschide Despre PairDrop",
"theme-light_title": "Utilizați mereu tema luminoasă"
},
"instructions": {
"x-instructions_mobile": "Atingeți pentru a trimite fișiere sau atingeți lung pentru a trimite un mesaj",
"click-to-send": "Clic pentru a trimite",
"activate-paste-mode-and-other-files": "și {{count}} alte fișiere",
"tap-to-send": "Atinge pentru a trimite",
"activate-paste-mode-base": "Deschideți PairDrop pe alte dispozitive pentru a trimite",
"no-peers-subtitle": "Împerecheați dispozitive sau intrați într-o cameră publică pentru a fi descoperit în alte rețele",
"activate-paste-mode-shared-text": "text partajat",
"x-instructions_desktop": "Dați clic pentru a trimite fișiere sau dați clic dreapta pentru a trimite un mesaj",
"no-peers-title": "Deschideți PairDrop pe alte dispozitive pentru a trimite fișiere",
"x-instructions_data-drop-peer": "Eliberare pentru a trimite la peer",
"x-instructions_data-drop-bg": "Eliberați pentru a selecta recipientul",
"no-peers_data-drop-bg": "Eliberare pentru a selecta recipientul"
},
"peer-ui": {
"processing": "Procesarea…",
"click-to-send-paste-mode": "Apasă pentru a trimite {{descriptor}}",
"click-to-send": "Apasă pentru a trimite fișiere sau apasă cu butonul din dreapta pentru a trimite un mesaj",
"waiting": "Așteptând…",
"connection-hash": "Pentru a verifica securitatea criptării end-to-end, comparați acest număr de securitate pe ambele dispozitive",
"preparing": "Pregătirea…",
"transferring": "Transferul…"
},
"dialogs": {
"base64-paste-to-send": "Lipiți aici pentru a trimite {{type}}",
"auto-accept-instructions-2": "pentru a accepta automat toate fișierele trimise de la dispozitivul respectiv.",
"receive-text-title": "Mesaj primit",
"edit-paired-devices-title": "Editați dispozitivele asociate",
"cancel": "Anulează",
"auto-accept-instructions-1": "Activează",
"pair-devices-title": "Împerecherea permanentă a dispozitivelor",
"download": "Descarcă",
"title-file": "Fişier",
"base64-processing": "Procesarea…",
"decline": "Declin",
"receive-title": "{{descriptor}} Primit",
"leave": "Pleacă",
"join": "Alătură-te",
"title-image-plural": "Imagini",
"send": "Trimite",
"base64-tap-to-paste": "Atinge aici pentru a lipi {{type}}",
"base64-text": "text",
"copy": "Copiază",
"file-other-description-image": "și 1 altă imagine",
"temporary-public-room-title": "Cameră publică temporară",
"base64-files": "fişiere",
"has-sent": "a trimis:",
"file-other-description-file": "și 1 alt fișier",
"close": "Închide",
"system-language": "Limba Sistemului",
"unpair": "Decuplează",
"title-image": "Imagine",
"file-other-description-file-plural": "și {{count}} alte fișiere",
"would-like-to-share": "ar dori să împărtășească",
"send-message-to": "Trimite un mesaj la",
"language-selector-title": "Setați Limba",
"pair": "Cuplu",
"hr-or": "SAU",
"scan-qr-code": "sau scanați codul QR.",
"input-key-on-this-device": "Introduceți această cheie pe un alt dispozitiv",
"download-again": "Descarcă din nou",
"accept": "Acceptă",
"paired-devices-wrapper_data-empty": "Nu sunt dispozitive asociate.",
"enter-key-from-another-device": "Introduceți aici cheia de la un alt dispozitiv.",
"share": "Partajați",
"auto-accept": "auto-acceptare",
"title-file-plural": "Fişiere",
"send-message-title": "Trimite un mesaj",
"input-room-id-on-another-device": "Introduceți acest ID de cameră pe un alt dispozitiv",
"file-other-description-image-plural": "și {{count}} alte imagini",
"enter-room-id-from-another-device": "Introdu ID-ul camerei de pe un alt dispozitiv pentru a intra în cameră.",
"message_title": "Inserați mesajul de trimis",
"pair-devices-qr-code_title": "Dați clic pentru a copia link-ul pentru a asocia acest dispozitiv",
"public-room-qr-code_title": "Dați clic pentru a copia link-ul în sala publică"
},
"about": {
"claim": "Cel mai simplu mod de a transfera fișiere între dispozitive",
"tweet_title": "Tweet despre PairDrop",
"close-about_aria-label": "Închide Despre PairDrop",
"buy-me-a-coffee_title": "Cumpără-mi o cafea!",
"github_title": "PairDrop pe GitHub",
"faq_title": "Întrebări frecvente"
},
"document-titles": {
"file-transfer-requested": "Transfer de fișiere cerut",
"message-received-plural": "{{count}}} Mesaje primite",
"message-received": "Mesaj primit",
"file-received": "Fișier Primit",
"file-received-plural": "{{count}} Fișiere Primite",
"image-transfer-requested": "Transfer de imagine solicitat"
}
}

View File

@@ -0,0 +1,167 @@
{
"header": {
"about_aria-label": "Открыть страницу \"О сервисе\"",
"pair-device_title": "Связать ваши устройства навсегда",
"install_title": "Установить PairDrop",
"cancel-paste-mode": "Выполнено",
"edit-paired-devices_title": "Редактировать связанные устройства",
"notification_title": "Включить уведомления",
"about_title": "О сервисе",
"theme-auto_title": "Адаптировать тему к системной автоматически",
"theme-dark_title": "Всегда использовать темную тему",
"theme-light_title": "Всегда использовать светлую тему",
"join-public-room_title": "Войти на время в публичную комнату",
"language-selector_title": "Выбрать язык"
},
"instructions": {
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
"click-to-send": "Нажмите, чтобы отправить",
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
"tap-to-send": "Прикоснитесь, чтобы отправить",
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
"no-peers-subtitle": "Свяжите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
"activate-paste-mode-shared-text": "общий текст"
},
"footer": {
"display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик",
"paired-devices": "связанными устройствами",
"known-as": "Вы известны под именем:",
"on-this-network": "в этой сети",
"display-name_title": "Изменить имя вашего устройства навсегда",
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
"paired-devices_title": "Вы можете быть обнаружены связанными устройствами в любое время вне зависимости от сети.",
"public-room-devices": "в комнате {{roomId}}",
"discovery": "Вы можете быть обнаружены:",
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
},
"dialogs": {
"edit-paired-devices-title": "Редактировать Связанные Устройства",
"auto-accept": "автоприем",
"close": "Закрыть",
"decline": "Отклонить",
"share": "Поделиться",
"would-like-to-share": "хотел бы поделиться",
"has-sent": "отправил:",
"paired-devices-wrapper_data-empty": "Нет связанных устройств.",
"download": "Скачать",
"receive-text-title": "Сообщение получено",
"send": "Отправить",
"send-message-to": "Отправить сообщение",
"send-message-title": "Отправить сообщение",
"copy": "Копировать",
"base64-files": "файлы",
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
"base64-processing": "Обработка…",
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
"base64-text": "текст",
"title-file": "Файл",
"title-file-plural": "Файлы",
"title-image": "Изображение",
"title-image-plural": "Изображения",
"download-again": "Скачать еще раз",
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
"pair-devices-title": "Соединить устройства навсегда",
"input-key-on-this-device": "На другом устройстве введите этот ключ",
"scan-qr-code": "или отсканируйте QR-код.",
"cancel": "Отменить",
"pair": "Подключить",
"accept": "Принять",
"auto-accept-instructions-1": "Активировать",
"file-other-description-file": "и 1 другой файл",
"file-other-description-image-plural": "и {{count}} других изображений",
"file-other-description-image": "и 1 другое изображение",
"file-other-description-file-plural": "и {{count}} других файлов",
"receive-title": "{{descriptor}} получен",
"system-language": "Язык системы",
"unpair": "Отвязать",
"language-selector-title": "Установить язык",
"hr-or": "ИЛИ",
"input-room-id-on-another-device": "На другом устройстве введите этот ID комнаты",
"leave": "Покинуть",
"join": "Войти",
"enter-room-id-from-another-device": "Введите ID комнаты с другого устройства, чтобы войти в нее.",
"temporary-public-room-title": "Временная публичная комната",
"message_title": "Вставьте сообщение для отправки",
"pair-devices-qr-code_title": "Нажмите, чтобы скопировать ссылку для привязки этого устройства",
"public-room-qr-code_title": "Нажмите, чтобы скопировать ссылку на публичную комнату"
},
"about": {
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
"claim": "Самый простой способ передачи файлов между устройствами",
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
"buy-me-a-coffee_title": "Купить мне кофе!",
"github_title": "PairDrop на GitHub",
"tweet_title": "Твит о PairDrop",
"faq_title": "Часто задаваемые вопросы"
},
"notifications": {
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
"pairing-success": "Устройства связаны.",
"pairing-tabs-error": "Связка двух вкладок браузера невозможна.",
"copied-to-clipboard": "Скопировано в буфер обмена",
"pairing-not-persistent": "Связанные устройства непостоянны.",
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
"notifications-enabled": "Уведомления включены.",
"text-content-incorrect": "Содержание текста неверно.",
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
"connected": "Подключено.",
"copied-text": "Текст скопирован в буфер обмена",
"online": "Вы снова в сети",
"offline": "Вы находитесь вне сети",
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
"files-incorrect": "Файлы неверны.",
"message-transfer-completed": "Передача сообщения завершена.",
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
"selected-peer-left": "Выбранный узел вышел.",
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
"pairing-cleared": "Все устройства отвязаны.",
"pairing-key-invalid": "Неверный ключ",
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
"click-to-download": "Нажмите, чтобы скачать",
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
"click-to-show": "Нажмите, чтобы показать",
"connecting": "Подключение…",
"download-successful": "{{descriptor}} загружен",
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
"file-content-incorrect": "Содержимое файла неверно.",
"file-transfer-completed": "Передача файла завершена.",
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
"public-room-id-invalid": "Неверный ID комнаты",
"online-requirement-pairing": "Для связки устройств необходимо находиться быть онлайн.",
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн.",
"notifications-permissions-error": "Уведомления были заблокированы, так как пользователь отклонил запрос на их работу несколько раз. Это можно изменить в меню \"О странице\", которое может быть вызвано нажатием на иконку замочка рядом со строкой адреса сайта.",
"pair-url-copied-to-clipboard": "Ссылка для привязки этого устройства была скопирована в буфер обмена",
"room-url-copied-to-clipboard": "Ссылка на публичную комнату была скопирована в буфер обмена"
},
"peer-ui": {
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
"preparing": "Подготовка…",
"transferring": "Передача…",
"processing": "Обработка…",
"waiting": "Ожидание…",
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
},
"document-titles": {
"file-received-plural": "{{count}} файлов получено",
"message-received-plural": "{{count}} сообщений получено",
"file-received": "Файл получен",
"file-transfer-requested": "Запрошена передача файлов",
"message-received": "Сообщение получено",
"image-transfer-requested": "Запрошена передача изображений"
}
}

View File

@@ -0,0 +1,25 @@
{
"header": {
"about_title": "PairDrop Hakkında",
"about_aria-label": "PairDrop Hakkında Aç",
"theme-auto_title": "Temayı Sisteme Uyarla",
"theme-light_title": "Daima Açık Tema Kullan",
"theme-dark_title": "Daima Koyu Tema Kullan",
"notification_title": "Bildirimleri Etkinleştir",
"install_title": "PairDrop'u Yükle",
"pair-device_title": "Cihaz Eşleştir",
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
"cancel-paste-mode": "Bitti"
},
"instructions": {
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
},
"footer": {
"display-name_data-placeholder": "Yükleniyor…",
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
},
"dialogs": {
"cancel": "İptal",
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
}
}

View File

@@ -0,0 +1,166 @@
{
"header": {
"about_title": "关于 PairDrop",
"about_aria-label": "打开 关于 PairDrop",
"theme-light_title": "总是使用明亮主题",
"install_title": "安装 PairDrop",
"pair-device_title": "永久配对您的设备",
"theme-auto_title": "主题适应系统",
"theme-dark_title": "总是使用暗黑主题",
"notification_title": "开启通知",
"edit-paired-devices_title": "管理已配对设备",
"cancel-paste-mode": "完成",
"join-public-room_title": "暂时加入公共房间",
"language-selector_title": "设置语言"
},
"instructions": {
"x-instructions_data-drop-peer": "释放以发送到此设备",
"no-peers_data-drop-bg": "释放来选择接收者",
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 以便在其他网络上可见",
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
"x-instructions_data-drop-bg": "释放来选择接收者",
"click-to-send": "点击发送",
"tap-to-send": "轻触发送",
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
"activate-paste-mode-shared-text": "分享文本"
},
"footer": {
"routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:",
"display-name_data-placeholder": "加载中…",
"display-name_title": "修改你的默认设备名",
"on-this-network": "在此网络上",
"paired-devices": "已配对的设备",
"traffic": "流量将",
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
"public-room-devices": "在房间 {{roomId}} 中",
"discovery": "您可以被发现:",
"on-this-network_title": "您可以被这个网络上的每个人发现。"
},
"dialogs": {
"pair-devices-title": "配对新设备(常驻)",
"input-key-on-this-device": "在另一个设备上输入这串数字",
"base64-text": "信息",
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
"edit-paired-devices-title": "管理已配对的设备",
"pair": "配对",
"cancel": "取消",
"scan-qr-code": "或者 扫描二维码。",
"paired-devices-wrapper_data-empty": "无已配对设备。",
"auto-accept-instructions-1": "启用",
"auto-accept": "自动接收",
"decline": "拒绝",
"base64-processing": "处理中…",
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
"would-like-to-share": "想要分享",
"accept": "接收",
"close": "关闭",
"share": "分享",
"download": "保存",
"send": "发送",
"receive-text-title": "收到信息",
"copy": "复制",
"send-message-title": "发送信息",
"send-message-to": "发了一条信息给",
"has-sent": "发送了:",
"base64-files": "文件",
"file-other-description-file": "和 1 个其他的文件",
"file-other-description-image": "和 1 个其他的图片",
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
"title-image-plural": "图片",
"receive-title": "收到 {{descriptor}}",
"title-image": "图片",
"title-file": "文件",
"title-file-plural": "文件",
"download-again": "再次保存",
"system-language": "跟随系统语言",
"unpair": "取消配对",
"language-selector-title": "设置语言",
"hr-or": "或者",
"input-room-id-on-another-device": "在另一个设备上输入这串房间号",
"leave": "离开",
"join": "加入",
"temporary-public-room-title": "临时公共房间",
"enter-room-id-from-another-device": "在另一个设备上输入这串房间号来加入房间。",
"message_title": "插入要发送的消息",
"pair-devices-qr-code_title": "单击复制和此设备配对的链接",
"public-room-qr-code_title": "单击复制公共房间链接"
},
"about": {
"faq_title": "常见问题",
"close-about_aria-label": "关闭 关于 PairDrop",
"github_title": "PairDrop 在 GitHub 上开源",
"claim": "最简单的跨设备传输方案",
"buy-me-a-coffee_title": "帮我买杯咖啡!",
"tweet_title": "关于 PairDrop 的推特"
},
"notifications": {
"display-name-changed-permanently": "展示的名字已经永久变更。",
"display-name-changed-temporarily": "展示的名字仅在此会话中变更。",
"display-name-random-again": "展示的名字已再次随机生成。",
"download-successful": "{{descriptor}} 已下载",
"pairing-tabs-error": "无法配对两个浏览器标签页。",
"pairing-success": "新设备已配对。",
"pairing-not-persistent": "配对的设备不是持久的。",
"pairing-key-invalid": "无效配对码",
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
"text-content-incorrect": "文本内容不合法。",
"file-content-incorrect": "文件内容不合法。",
"clipboard-content-incorrect": "剪贴板内容不合法。",
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
"click-to-show": "点击展示",
"copied-text": "复制到剪贴板",
"selected-peer-left": "选择的设备已离开。",
"pairing-cleared": "所有设备已解除配对。",
"copied-to-clipboard": "已复制到剪贴板",
"notifications-enabled": "通知已启用。",
"copied-text-error": "写入剪贴板失败。请手动复制!",
"click-to-download": "点击以保存",
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
"message-transfer-completed": "信息传输已完成。",
"offline": "你未连接到网络",
"online": "你已重新连接到网络",
"connected": "已连接。",
"online-requirement": "你需要连接网络来配对新设备。",
"files-incorrect": "文件不合法。",
"file-transfer-completed": "文件传输已完成。",
"connecting": "连接中…",
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
"public-room-left": "已退出公共房间 {{publicRoomId}}",
"copied-to-clipboard-error": "无法复制。请手动复制。",
"public-room-id-invalid": "无效的房间号",
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。",
"notifications-permissions-error": "因用户数次拒绝了权限授予提示,通知权限已被拦截。可以在“页面信息”中重置它,要访问“页面信息”请单击地址栏旁的挂锁图标。",
"pair-url-copied-to-clipboard": "已将和此设备配对的链接复制到剪贴板",
"room-url-copied-to-clipboard": "已将公共房间的链接复制到剪贴板"
},
"document-titles": {
"message-received": "收到信息",
"message-received-plural": "收到 {{count}} 条信息",
"file-transfer-requested": "文件传输请求",
"file-received-plural": "收到 {{count}} 个文件",
"file-received": "收到文件",
"image-transfer-requested": "图片传输请求"
},
"peer-ui": {
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
"click-to-send": "点击以发送文件 或 右键来发送信息",
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
"preparing": "准备中…",
"waiting": "请等待…",
"transferring": "传输中…",
"processing": "处理中…"
}
}

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,172 @@
class Localization {
constructor() {
Localization.defaultLocale = "en";
Localization.supportedLocales = ["ar", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "ro", "ru", "zh-CN"];
Localization.supportedLocalesRTL = ["ar"];
Localization.translations = {};
Localization.defaultTranslations = {};
Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages);
let storedLanguageCode = localStorage.getItem("language-code");
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
? storedLanguageCode
: Localization.systemLocale;
Localization.setTranslation(Localization.initialLocale)
.then(_ => {
console.log("Initial translation successful.");
Events.fire("initial-translation-loaded");
});
}
static isSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
}
static isRTLLanguage(locale) {
return Localization.supportedLocalesRTL.indexOf(locale) > -1;
}
static getSupportedOrDefault(locales) {
let localesGeneric = locales
.map(locale => locale.split("-")[0])
.filter(locale => locales.indexOf(locale) === -1);
return locales.find(Localization.isSupported)
|| localesGeneric.find(Localization.isSupported)
|| Localization.defaultLocale;
}
static async setTranslation(locale) {
if (!locale) locale = Localization.systemLocale;
await Localization.setLocale(locale)
await Localization.translatePage();
const htmlRootNode = document.querySelector('html');
if (Localization.isRTLLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
} else {
htmlRootNode.removeAttribute('dir');
}
htmlRootNode.setAttribute('lang', locale);
console.log("Page successfully translated",
`System language: ${Localization.systemLocale}`,
`Selected language: ${locale}`
);
Events.fire("translation-loaded");
}
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
if(!newTranslations) return false;
Localization.locale = newLocale;
Localization.translations = newTranslations;
}
static getLocale() {
return Localization.locale;
}
static isSystemLocale() {
return !localStorage.getItem('language-code');
}
static async fetchTranslationsFor(newLocale) {
const response = await fetch(`lang/${newLocale}.json`, {
method: 'GET',
credentials: 'include',
mode: 'no-cors',
});
if (response.redirected === true || response.status !== 200) return false;
return await response.json();
}
static async translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
}
static async translateElement(element) {
const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) {
let attr = attrs[i];
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
} else {
if (attr.startsWith("data-")) {
let dataAttr = attr.substring(5);
element.dataset.dataAttr = Localization.getTranslation(key, attr);
} {
element.setAttribute(attr, Localization.getTranslation(key, attr));
}
}
}
}
static getTranslation(key, attr=null, data={}, useDefault=false) {
const keys = key.split(".");
let translationCandidates = useDefault
? Localization.defaultTranslations
: Localization.translations;
let translation;
try {
for (let i = 0; i < keys.length - 1; i++) {
translationCandidates = translationCandidates[keys[i]]
}
let lastKey = keys[keys.length - 1];
if (attr) lastKey += "_" + attr;
translation = translationCandidates[lastKey];
for (let j in data) {
translation = translation.replace(`{{${j}}}`, data[j]);
}
} catch (e) {
translation = "";
}
if (!translation) {
if (!useDefault) {
translation = this.getTranslation(key, attr, data, true);
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
} else {
console.warn("Missing translation in default language:", key, attr);
}
}
return Localization.escapeHTML(translation);
}
static escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML;
}
}

View File

@@ -1,27 +1,47 @@
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
Events.on('create-public-room', _ => this._onCreatePublicRoom());
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
Events.on('online', _ => this._connect());
}
_connect() {
clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return;
if (this._isConnected() || this._isConnecting() || this._isOffline()) return;
if (this._isReconnect) {
Events.fire('notify-user', {
message: Localization.getTranslation("notifications.connecting"),
persistent: true
});
}
const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen();
@@ -34,27 +54,47 @@ class ServerConnection {
_onOpen() {
console.log('WS: server connected');
Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
}
_onPairDeviceInitiate() {
if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.');
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
return;
}
this.send({ type: 'pair-device-initiate' })
this.send({ type: 'pair-device-initiate' });
}
_onPairDeviceJoin(roomKey) {
_onPairDeviceJoin(pairKey) {
if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
return;
}
this.send({ type: 'pair-device-join', roomKey: roomKey })
this.send({ type: 'pair-device-join', pairKey: pairKey });
}
_onCreatePublicRoom() {
if (!this._isConnected()) {
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
return;
}
this.send({ type: 'create-public-room' });
}
_onJoinPublicRoom(roomId, createIfInvalid) {
if (!this._isConnected()) {
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
return;
}
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
}
_onLeavePublicRoom() {
if (!this._isConnected()) {
setTimeout(_ => this._onLeavePublicRoom(), 1000);
return;
}
this.send({ type: 'leave-public-room' });
}
_setRtcConfig(config) {
@@ -63,13 +103,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -96,14 +136,26 @@ class ServerConnection {
Events.fire('pair-device-join-key-invalid');
break;
case 'pair-device-canceled':
Events.fire('pair-device-canceled', msg.roomKey);
Events.fire('pair-device-canceled', msg.pairKey);
break;
case 'pair-device-join-key-rate-limit':
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
case 'join-key-rate-limit':
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
break;
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
case 'public-room-id-invalid':
Events.fire('public-room-id-invalid', msg.publicRoomId);
break;
case 'public-room-created':
Events.fire('public-room-created', msg.roomId);
break;
case 'public-room-left':
Events.fire('public-room-left');
break;
case 'request':
case 'header':
case 'partition':
@@ -118,18 +170,37 @@ class ServerConnection {
Events.fire('ws-relay', JSON.stringify(msg));
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
}
_onDisplayName(msg) {
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem('peer_id', msg.message.peerId);
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -138,8 +209,8 @@ class ServerConnection {
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = sessionStorage.getItem("peerId");
const peerIdHash = sessionStorage.getItem("peerIdHash");
const peerId = sessionStorage.getItem('peer_id');
const peerIdHash = sessionStorage.getItem('peer_id_hash');
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
@@ -149,26 +220,32 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
const peerId = sessionStorage.getItem('peer_id');
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
console.log('WS: server disconnected');
setTimeout(() => {
this._isReconnect = true;
Events.fire('ws-disconnected');
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
}, 100); //delay for 100ms to prevent flickering on page reload
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -180,6 +257,10 @@ class ServerConnection {
return this._socket && this._socket.readyState === this._socket.CONNECTING;
}
_isOffline() {
return !navigator.onLine;
}
_onError(e) {
console.error(e);
}
@@ -192,13 +273,19 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._roomIds = {};
this._updateRoomIds(roomType, roomId);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -209,12 +296,73 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_isPaired() {
return !!this._roomIds['secret'];
}
_getPairSecret() {
return this._roomIds['secret'];
}
_getRoomTypes() {
return Object.keys(this._roomIds);
}
_updateRoomIds(roomType, roomId) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
// multiple roomSecrets with same peer -> delete old roomSecret
PersistentStorage.deleteRoomSecret(this._getPairSecret())
.then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
});
}
this._roomIds[roomType] = roomId;
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._getPairSecret());
}
}
_removeRoomType(roomType) {
delete this._roomIds[roomType];
Events.fire('room-type-removed', {
peerId: this._peerId,
roomType: roomType
});
}
_evaluateAutoAccept() {
if (!this._isPaired()) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry
? roomSecretEntry.entry.auto_accept
: false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = !this._isSameBrowser()
? autoAccept
: false;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -259,7 +407,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -371,7 +523,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -383,6 +535,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -413,7 +573,7 @@ class Peer {
_abortTransfer() {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
Events.fire('notify-user', 'Files are incorrect.');
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
this._filesReceived = [];
this._requestAccepted = null;
this._digester = null;
@@ -454,14 +614,14 @@ class Peer {
this._abortTransfer();
}
// include for compatibility with Snapdrop for Android app
// include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob);
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@@ -471,7 +631,8 @@ class Peer {
this._chunker = null;
if (!this._filesQueue.length) {
this._busy = false;
Events.fire('notify-user', 'File transfer completed.');
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else {
this._dequeueFile();
}
@@ -482,7 +643,7 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
this._filesRequested = null;
if (message.reason === 'ios-memory-limit') {
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
}
return;
}
@@ -492,7 +653,7 @@ class Peer {
}
_onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.');
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
}
sendText(text) {
@@ -508,34 +669,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -545,13 +711,17 @@ class RTCPeer extends Peer {
_openChannel() {
if (!this._conn) return;
const channel = this._conn.createDataChannel('data-channel', {
ordered: true,
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
});
channel.onopen = e => this._onChannelOpened(e);
channel.onerror = e => this._onError(e);
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
this._conn.createOffer()
.then(d => this._onDescription(d))
.catch(e => this._onError(e));
}
_onDescription(description) {
@@ -567,7 +737,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -632,7 +802,7 @@ class RTCPeer extends Peer {
_onBeforeUnload(e) {
if (this._busy) {
e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?";
return Localization.getTranslation("notifications.unfinished-transfers-warning");
}
}
@@ -652,7 +822,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -691,15 +861,19 @@ class RTCPeer extends Peer {
_sendSignal(signal) {
signal.type = 'signal';
signal.to = this._peerId;
signal.roomType = this._roomType;
signal.roomSecret = this._roomSecret;
signal.roomType = this._getRoomTypes()[0];
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(signal);
}
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -718,11 +892,10 @@ class RTCPeer extends Peer {
class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller
this._isCaller = true;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal();
}
@@ -734,10 +907,9 @@ class WSPeer extends Peer {
}
sendJSON(message) {
console.debug(message)
message.to = this._peerId;
message.roomType = this._roomType;
message.roomSecret = this._roomSecret;
message.roomType = this._getRoomTypes()[0];
message.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(message);
}
@@ -769,49 +941,77 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
// this device closes connection
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
// peer closes connection
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
if (window.isRtcSupported && message.sender.rtcSupported) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
} else {
this.peers[message.sender.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret);
}
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_refreshPeer(peer, roomType, roomId) {
if (!peer) return false;
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
// if roomType or roomId for roomType differs peer is already connected
// -> only update roomSecret and reevaluate auto accept
if (roomTypesDiffer || roomIdsDiffer) {
peer._updateRoomIds(roomType, roomId);
peer._evaluateAutoAccept();
return true;
}
this.peers[message.sender.id].onServerMessage(message);
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomId);
return;
}
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomId);
} else {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
}
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
})
}
_onWsRelay(message) {
const messageJSON = JSON.parse(message)
const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message)
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
}
if (window.isRtcSupported && peer.rtcSupported) {
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
} else {
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
}
})
this.peers[messageJSON.sender.id]._onMessage(message);
}
_onRespondToFileTransferRequest(detail) {
@@ -838,13 +1038,22 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) {
_onPeerLeft(message) {
if (this.peers[message.peerId] && (!this.peers[message.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', message.peerId);
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -854,7 +1063,6 @@ class PeersManager {
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
@@ -868,17 +1076,52 @@ class PeersManager {
if (peer._channel) peer._channel.onclose = null;
peer._conn.close();
peer._busy = false;
peer._roomIds = {};
}
_onRoomSecretsDeleted(roomSecrets) {
for (let i=0; i<roomSecrets.length; i++) {
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
}
}
_onLeavePublicRoom(publicRoomId) {
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
}
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
}
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
const peerIds = this._getPeerIdsFromRoomId(roomId);
if (!peerIds.length) return;
for (let i=0; i<peerIds.length; i++) {
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
}
}
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
const peer = this.peers[peerId];
if (!peer) return;
if (peer._getRoomTypes().length > 1) {
peer._removeRoomType(roomType);
} else {
Events.fire('peer-disconnected', peerId);
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -894,6 +1137,31 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdsFromRoomId(roomId) {
if (!roomId) return [];
let peerIds = []
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomId.
if (Object.values(peer._roomIds).includes(roomId)) {
peerIds.push(peer._peerId);
}
}
return peerIds;
}
}
@@ -979,7 +1247,7 @@ class FileDigester {
}
class Events {
static fire(type, detail) {
static fire(type, detail = {}) {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}

View File

@@ -1,39 +1,78 @@
(function(){
// Select the button
const btnTheme = document.getElementById('theme');
// Check for dark mode preference at the OS level
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
// Get the user's theme preference from local storage, if it's available
const currentTheme = localStorage.getItem('theme');
// If the user's preference in localStorage is dark...
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
const $themeAuto = document.getElementById('theme-auto');
const $themeLight = document.getElementById('theme-light');
const $themeDark = document.getElementById('theme-dark');
let currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
// ...let's toggle the .dark-theme class on the body
document.body.classList.toggle('dark-theme');
// Otherwise, if the user's preference in localStorage is light...
setModeToDark();
} else if (currentTheme === 'light') {
// ...let's toggle the .light-theme class on the body
document.body.classList.toggle('light-theme');
setModeToLight();
}
// Listen for a click on the button
btnTheme.addEventListener('click', function(e) {
e.preventDefault();
// If the user's OS setting is dark and matches our .dark-theme class...
let theme;
if (prefersDarkScheme.matches) {
// ...then toggle the light mode class
document.body.classList.toggle('light-theme');
// ...but use .dark-theme if the .light-theme class is already on the body,
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
$themeAuto.addEventListener('click', _ => {
if (currentTheme) {
setModeToAuto();
} else {
// Otherwise, let's do the same thing, but for .dark-theme
document.body.classList.toggle('dark-theme');
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
setModeToDark();
}
});
$themeLight.addEventListener('click', _ => {
if (currentTheme !== 'light') {
setModeToLight();
} else {
setModeToAuto();
}
});
$themeDark.addEventListener('click', _ => {
if (currentTheme !== 'dark') {
setModeToDark();
} else {
setModeToLight();
}
// Finally, let's save the current preference to localStorage to keep using it
localStorage.setItem('theme', theme);
});
function setModeToDark() {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
localStorage.setItem('theme', 'dark');
currentTheme = 'dark';
$themeAuto.classList.remove("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.add("selected");
}
function setModeToLight() {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
localStorage.setItem('theme', 'light');
currentTheme = 'light';
$themeAuto.classList.remove("selected");
$themeLight.classList.add("selected");
$themeDark.classList.remove("selected");
}
function setModeToAuto() {
document.body.classList.remove('dark-theme');
document.body.classList.remove('light-theme');
if (prefersDarkTheme) {
document.body.classList.add('dark-theme');
} else if (prefersLightTheme) {
document.body.classList.add('light-theme');
}
localStorage.removeItem('theme');
currentTheme = undefined;
$themeAuto.classList.add("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.remove("selected");
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -399,6 +399,19 @@ const cyrb53 = function(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
function changeFavicon(src) {
document.querySelector('[rel="icon"]').href = src;
document.querySelector('[rel="shortcut icon"]').href = src;
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
@@ -410,11 +423,11 @@ function arrayBufferToBase64(buffer) {
}
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
}

View File

@@ -1,16 +1,19 @@
const cacheVersion = 'v1.5.0';
const cacheVersion = 'v1.9.4';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
const urlsToCache = [
'index.html',
'./',
'index.html',
'manifest.json',
'styles.css',
'scripts/localization.js',
'scripts/network.js',
'scripts/NoSleep.min.js',
'scripts/QRCode.min.js',
'scripts/theme.js',
'scripts/ui.js',
'scripts/util.js',
'scripts/qrcode.js',
'scripts/zip.min.js',
'scripts/NoSleep.min.js',
'scripts/theme.js',
'sounds/blop.mp3',
'images/favicon-96x96.png',
'images/favicon-96x96-notification.png',
@@ -19,6 +22,19 @@ const urlsToCache = [
'images/android-chrome-512x512.png',
'images/android-chrome-512x512-maskable.png',
'images/apple-touch-icon.png',
'lang/ar.json',
'lang/de.json',
'lang/en.json',
'lang/es.json',
'lang/fr.json',
'lang/id.json',
'lang/it.json',
'lang/ja.json',
'lang/nb.json',
'lang/nl.json',
'lang/ro.json',
'lang/ru.json',
'lang/zh-CN.json'
];
self.addEventListener('install', function(event) {
@@ -40,7 +56,7 @@ const fromNetwork = (request, timeout) =>
fetch(request).then(response => {
clearTimeout(timeoutId);
fulfill(response);
update(request);
update(request).then(() => console.log("Cache successfully updated for", request.url));
}, reject);
});
@@ -49,9 +65,7 @@ const fromCache = request =>
caches
.open(cacheTitle)
.then(cache =>
cache
.match(request)
.then(matching => matching || cache.match('/offline/'))
cache.match(request)
);
// cache the current page to make it available for offline
@@ -59,63 +73,98 @@ const update = request =>
caches
.open(cacheTitle)
.then(cache =>
fetch(request).then(response => {
cache.put(request, response).then(_ => {
console.log("Page successfully cached.")
fetch(request)
.then(async response => {
await cache.put(request, response);
})
})
.catch(() => console.log(`Cache could not be updated. ${request.url}`))
);
// general strategy when making a request (eg if online try to fetch it
// from the network with a timeout, if something fails serve from cache)
// from cache, if something fails fetch from network. Update cache everytime files are fetched.
// This way files should only be fetched if cacheVersion is changed
self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith(
(async () => {
const formData = await event.request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.get("files");
let share_url = "/";
if (files.length > 0) {
share_url = "/?share-target=files";
const db = await window.indexedDB.open('pairdrop_store');
const tx = db.transaction('share_target_files', 'readwrite');
const store = tx.objectStore('share_target_files');
for (let i=0; i<files.length; i++) {
await store.add(files[i]);
}
await tx.complete
db.close()
} else if (title.length > 0 || text.length > 0 || url.length) {
share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`;
}
return Response.redirect(encodeURI(share_url), 303);
})()
);
event.respondWith((async () => {
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
// Regular requests not related to Web Share Target.
event.respondWith(
fromNetwork(event.request, 10000).catch(() => fromCache(event.request))
);
event.waitUntil(update(event.request));
if (forceFetch) {
event.respondWith(fromNetwork(event.request, 10000));
} else {
event.respondWith(
fromCache(event.request).then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
);
}
}
});
// on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt =>
evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
)
self.addEventListener('activate', evt => {
return evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
)
}
);
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
fileObjects.push({
name: files[i].name,
buffer: await files[i].arrayBuffer()
});
}
const DBOpenRequest = indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
for (let i = 0; i < fileObjects.length; i++) {
const transaction = db.transaction('share_target_files', 'readwrite');
const objectStore = transaction.objectStore('share_target_files');
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve(pairDropUrl);
}
} else {
let urlArgument = '?share-target=text';
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(pairDropUrl + urlArgument);
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,9 @@
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "stun:openrelay.metered.ca:80"
},
{
"urls": "turn:openrelay.metered.ca:443",
"username": "openrelayproject",
"credential": "openrelayproject"
"urls": "turn:example.com:3478",
"username": "username",
"credential": "password"
}
]
}

38
turnserver_example.conf Normal file
View File

@@ -0,0 +1,38 @@
# TURN server name and realm
realm=<DOMAIN>
server-name=pairdrop
# IPs the TURN server listens to
listening-ip=0.0.0.0
# External IP-Address of the TURN server
external-ip=<IP_ADDRESS>
# Main listening port
listening-port=3478
# Further ports that are open for communication
min-port=10000
max-port=20000
# Use fingerprint in TURN message
fingerprint
# Log file path
log-file=/var/log/turnserver.log
# Enable verbose logging
verbose
# Specify the user for the TURN authentification
user=user:password
# Enable long-term credential mechanism
lt-cred-mech
# SSL certificates
cert=/etc/letsencrypt/live/<DOMAIN>/cert.pem
pkey=/etc/letsencrypt/live/<DOMAIN>/privkey.pem
# 443 for TURN over TLS, which can bypass firewalls
tls-listening-port=443