Compare commits

...

59 Commits

Author SHA1 Message Date
schlagmichdoch 31ec776fb3 Only handle requests with the same origin via the service worker 2025-02-25 18:19:38 +01:00
schlagmichdoch 4862ba3067 Increase version to v1.11.2
## Enhancements
- Speed up update process by skipping waiting and claiming currently open pages -> no need to close all tabs in order to get an update anymore
- Update Bluesky icon from square to butterfly
- Update node dependencies

## Fixes
- Prevent service-worker from responding with redirect (fixes #384)
- Fix: switch off twitter button via env var not possible (#388)
- Add missing files to paths that get cached upon sw installation

## Languages
- Translations updates from Hosted Weblate (Japanese)
2025-02-24 20:46:32 +01:00
schlagmichdoch 7639aca84c Update node dependencies 2025-02-24 20:45:12 +01:00
schlagmichdoch 8b8e5b7433
Merge pull request #385 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2025-02-24 20:34:56 +01:00
Hosted Weblate f6f3db2df9
Translated using Weblate (Japanese)
Currently translated at 100.0% (166 of 166 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (166 of 166 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (166 of 166 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: maboroshin <maboroshin@users.noreply.hosted.weblate.org>
Co-authored-by: mottcha <89951503+mottcha@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ja/
Translation: PairDrop/pairdrop-spa
2025-02-24 20:34:40 +01:00
Hosted Weblate abd3a0c47c
Translated using Weblate (Bengali)
Currently translated at 17.4% (29 of 166 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Saif Mahmud <saifmahmud366@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/bn/
Translation: PairDrop/pairdrop-spa
2025-02-24 20:34:40 +01:00
schlagmichdoch 89734e6954
Merge pull request #389 from schlagmichdoch/button-configs
Fix buttons on about page
2025-02-24 20:33:44 +01:00
schlagmichdoch 59bd9265bc
Merge pull request #390 from schlagmichdoch/fix-sw-updates
Fix service worker updates and speed up updates
2025-02-24 20:33:20 +01:00
schlagmichdoch dbd6321fec Speed up update process by skipping waiting and claiming currently open pages -> no need to close all tabs in order to get an update anymore 2025-02-24 20:21:16 +01:00
schlagmichdoch d18e290ad4 Add missing files to paths that get cached upon sw installation 2025-02-24 20:18:53 +01:00
schlagmichdoch d7b68e214e Prevent fetch response and cache update if response is redirected 2025-02-24 20:17:54 +01:00
schlagmichdoch 148eb79ef0 Fix alphabetical sort in supportedLocales 2025-02-24 20:09:00 +01:00
schlagmichdoch 80615c533c Update Bluesky icon from square to butterfly 2025-02-23 19:20:50 +01:00
schlagmichdoch b0b091c4f7 FIX: switch off twitter button via env var not possible (#388) 2025-02-23 19:13:44 +01:00
schlagmichdoch c7b7badb3d Increase version to v1.11.1
## Fixes
- Fix PWA install button on chromium based browsers (#383) (Thanks @realchrislovett)
- Fix wrong file extension on chromium based browsers if mime type is not set (#355)

## Languages
- Translations updates from Hosted Weblate (Japanese)
2025-02-19 12:24:53 +01:00
schlagmichdoch 13e75f525d
Merge pull request #382 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2025-02-19 12:19:46 +01:00
schlagmichdoch b7c1893ac0
Merge pull request #381 from schlagmichdoch/fix-filename-wo-suffix
WIP:Fix chromium file extension if mime type is not set
2025-02-19 12:17:55 +01:00
Chris Lovett 341fa7fdf0
Restore desktop Chrome PWA button (#383)
* Restore desktop Chrome PWA button with start_url ./
2025-02-19 11:46:00 +01:00
Hosted Weblate aa09da3076
Translated using Weblate (Japanese)
Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: maboroshin <maboroshin@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ja/
Translation: PairDrop/pairdrop-spa
2025-02-18 13:34:01 +01:00
schlagmichdoch 547038c9bc Fix file name display if no file extension is present 2025-02-17 19:32:54 +01:00
schlagmichdoch a39d8cdc84 Fix chromium filename by defaulting mime to "application/octet-stream" 2025-02-17 19:20:17 +01:00
schlagmichdoch b3c61f4baf Increase version to v1.11.0
## Enhancements
- Make PWA standalone (#264)
- Bring back background animation after performance optimization (#285)
- Add support for Safari 11-15 (#358)
- Update Twitter icon and URL to (X) (Thanks @realchrislovett)
- Update node dependencies

## Fixes
- Fix pasting of files on background to invoke share-mode and make it available on Firefox (#370)
- Fix padding issue on EditPairedDevicesDialog and Base64Dialog
- Fix hydration of URLs into links in received messages that have exclamation marks in path

## Languages
- Translations updates from Hosted Weblate (Basque, Norwegian Bokmål)
2025-02-17 13:50:15 +01:00
schlagmichdoch ef61cc4dfe Update node dependencies 2025-02-17 13:33:07 +01:00
schlagmichdoch fa992498fb Revert "Prevent background animation from being cut on devices with a notch"
This reverts commit 09e4e5d289.
2025-02-17 13:15:51 +01:00
schlagmichdoch 800d492da5 Fix animation color on older webkit/blink based browsers 2025-02-17 12:36:49 +01:00
schlagmichdoch b8a973f037 Fix background animation size on 4k screens and decrease base opacity 2025-02-17 11:52:07 +01:00
schlagmichdoch 83549261f6
Merge pull request #369 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2025-02-17 11:09:15 +01:00
Hosted Weblate 9f4309c866
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tobbz <thetobbz@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/
Translation: PairDrop/pairdrop-spa
2025-02-17 11:08:40 +01:00
Hosted Weblate 43ce64d68a
Translated using Weblate (Bengali)
Currently translated at 1.2% (2 of 166 strings)

Added translation using Weblate (Bengali)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Saif Mahmud <saifmahmud366@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/bn/
Translation: PairDrop/pairdrop-spa
2025-02-17 11:08:39 +01:00
Hosted Weblate 61caa43ce9
Translated using Weblate (Basque)
Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: xabirequejo <xabi.rn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/eu/
Translation: PairDrop/pairdrop-spa
2025-02-17 11:08:38 +01:00
schlagmichdoch 0e574ae7fc
Merge pull request #378 from schlagmichdoch/bring_back_animation
Bring back optimized background animation
2025-02-17 11:06:29 +01:00
schlagmichdoch 3240921207 Merge branch 'master' into bring_back_animation 2025-02-17 11:04:58 +01:00
schlagmichdoch 3c7042da6d
Merge pull request #375 from schlagmichdoch/fix-bugs-1.11
Fix bugs for 1.11
2025-02-17 10:56:25 +01:00
schlagmichdoch 09e4e5d289 Prevent background animation from being cut on devices with a notch 2025-02-16 23:47:35 +01:00
schlagmichdoch ec0012ecd1 Speed up animation slightly 2025-02-16 23:43:50 +01:00
schlagmichdoch 8eea54f8dd Make sure older webkit/blink based browsers are able to render opacity of circles 2025-02-16 23:43:50 +01:00
schlagmichdoch ae68ede3f3 Put worker and libraries in subfolders 2025-02-16 23:43:50 +01:00
schlagmichdoch 16523843bd Use OffscreenCanvas on supported browsers to offload canvas drawing to a service worker thread 2025-02-16 23:43:41 +01:00
schlagmichdoch 829d63deda
Merge pull request #377 from realchrislovett/twit
Updated Twitter (X) icon and URL
2025-02-16 19:44:22 +01:00
schlagmichdoch f0e7250617 Use time based approach to smoothen reduced framerate 2025-02-16 00:52:34 +01:00
schlagmichdoch eca41f36c7
Merge pull request #374 from schlagmichdoch/pwa_standalone
Enable standalone mode for PWA
2025-02-15 00:00:15 +01:00
schlagmichdoch 1d0d3d0896
Merge pull request #376 from realchrislovett/patch-1
Remove bar at top of screen on iOS
2025-02-14 23:53:03 +01:00
schlagmichdoch 8a3c60d3a6 Bring back optimized background animation 2025-02-14 20:54:15 +01:00
schlagmichdoch e2f0ca6e45 Fix links in messages cut if ! in path; make regex more readable 2025-02-14 19:13:39 +01:00
Chris Lovett 27a61d589b
Remove bar at top of screen on iOS
On iOS, when the app runs as a PWA without this line, there will be a bar at the top of the screen. Implementing this setting will make for a more seamless UI on iOS, as documented below.

https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
2025-02-14 12:05:38 -05:00
Chris Lovett 02c95dde9d
Updated Twitter (X) icon and URL 2025-02-14 10:17:10 -05:00
schlagmichdoch dabfe58124 Fix word-break of base64zip button on mobile 2025-02-14 15:47:17 +01:00
schlagmichdoch 82b329fea8 Fix padding of auto-accept instruction 2025-02-14 15:36:35 +01:00
schlagmichdoch 8a833cd69d Make PWA standalone 2025-02-13 20:07:38 +01:00
schlagmichdoch 629328c2f6 Use css instead of JS to detect offset on high viewports 2025-02-13 20:07:25 +01:00
schlagmichdoch 8826893a42
Merge pull request #372 from schlagmichdoch/fix-share-mode
Fix share-mode and enable pasting for Firefox users
2025-02-13 13:04:11 +01:00
schlagmichdoch efc360e106 Remove text selection prevention to enable pasting for Firefox users 2025-02-13 12:54:17 +01:00
schlagmichdoch 351e7d42c9 Fix share mode by converting FileList to Array 2025-02-13 12:54:17 +01:00
schlagmichdoch ec520248b8 Remove duplicate instruction 2025-02-13 12:54:17 +01:00
schlagmichdoch 94096aa13c
Merge pull request #349 from schlagmichdoch/fix-ios15.3
Add support for iOS <15.3
2025-02-13 12:02:14 +01:00
schlagmichdoch f7fe303fa7 Fix background css for clients iOS < 13.1 2025-02-13 11:39:10 +01:00
schlagmichdoch 940da7948c Stop usage of public class fields in order to support Safari 13.1 2025-02-13 10:24:27 +01:00
schlagmichdoch c52eeda3ff
Add BrowserStack to README.md 2025-02-12 00:35:24 +01:00
schlagmichdoch 44c0f3dbaa Check for BroadcastChannel availability before instantiating it 2025-02-11 16:02:24 +01:00
25 changed files with 722 additions and 260 deletions

View File

@ -36,7 +36,7 @@ If applicable, add screenshots to help explain your problem.
**Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes
Version: v1.10.11
Version: v1.11.2
**Bug occurs on self-hosted PairDrop instance**
No | Yes
@ -44,7 +44,7 @@ No | Yes
**Self-Hosted Setup**
Proxy: Nginx | Apache2
Deployment: docker run | docker compose | npm run start:prod
Version: v1.10.11
Version: v1.11.2
**Additional context**
Add any other context about the problem here.

View File

@ -110,6 +110,7 @@ Connect to others in complex network situations, or over the Internet.
* [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep))
* [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any))
* [Weblate](https://weblate.org/) web-based localization tool
* [BrowserStack](https://www.browserstack.com/) This project is tested with BrowserStack
[FAQ](docs/faq.md)

View File

@ -45,11 +45,11 @@ This pairdrop-cli version was released alongside v1.10.4
#### Linux / Mac
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)
```shell
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/pairdrop-cli.zip"
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip"
```
or
```shell
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/pairdrop-cli.zip"
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip"
```
2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`
```shell

45
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "pairdrop",
"version": "1.10.11",
"version": "1.11.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.10.11",
"version": "1.11.2",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
@ -68,9 +68,9 @@
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -263,9 +263,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz",
"integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==",
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
"engines": {
"node": ">= 16"
},
@ -273,7 +273,7 @@
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": "4 || 5 || ^5.0.0-beta.1"
"express": "^4.11 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/finalhandler": {
@ -318,16 +318,16 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@ -758,9 +758,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "1.0.38",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz",
"integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==",
"version": "1.0.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
"integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
"funding": [
{
"type": "opencollective",
@ -775,6 +775,9 @@
"url": "https://github.com/sponsors/faisalman"
}
],
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
@ -812,9 +815,9 @@
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"engines": {
"node": ">=10.0.0"
},

View File

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

View File

@ -10,6 +10,7 @@
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="PairDrop">
<meta name="application-name" content="PairDrop">
<!-- Descriptions -->
@ -416,12 +417,12 @@
<h2 class="dialog-title" 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>
<div class="row center p-2">
<div class="font-subheading">
<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>
<div class="center row-reverse btn-row wrap">
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
@ -667,7 +668,7 @@
</svg>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.10.11</div>
<div class="font-subheading">v1.11.2</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
@ -681,9 +682,9 @@
<use xlink:href="#donation"></use>
</svg>
</a>
<a class="icon-button" id="twitter-btn" 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">
<a class="icon-button" id="x-twitter-btn" target="_blank" href="https://x.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>
<use xlink:href="#x-twitter"></use>
</svg>
</a>
<a class="icon-button" id="mastodon-btn" target="_blank" rel="noreferrer" data-i18n-key="about.mastodon" data-i18n-attrs="title" hidden>
@ -739,8 +740,8 @@
<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>
<symbol id="x-twitter">
<path d="M17.996,2.219l3.265,0l-7.13,8.148l8.388,11.088l-6.566,0l-5.147,-6.723l-5.882,6.723l-3.269,0l7.625,-8.716l-8.041,-10.52l6.733,0l4.647,6.146l5.377,-6.146Zm-1.146,17.285l1.808,-0l-11.671,-15.435l-1.942,0l11.805,15.435Z"></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>
@ -810,9 +811,9 @@
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"></path>
</symbol>
<symbol id="bluesky" viewBox="0 0 448 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M0 96C0 60.7 28.7 32 64 32H384c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96z"></path>
<symbol id="bluesky" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"/>
</symbol>
<symbol id="custom" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->

35
public/lang/bn.json Normal file
View File

@ -0,0 +1,35 @@
{
"header": {
"about_title": "পেয়ার ড্রপ সম্পর্কে",
"install_title": "পেয়ার ড্রপ ইন্সটল করুন",
"pair-device_title": "ডিভাইস স্থায়ী ভাবে যুক্ত করুন",
"cancel-share-mode": "বাতিল",
"theme-light_title": "সবসময় সাদা থিম ব্যাবহার",
"language-selector_title": "ভাষা সেট করুন",
"about_aria-label": "পেয়ারড্রপ সম্পর্কে",
"theme-auto_title": "থিমের ধরন ডিভাইস অনুযায়ী",
"theme-dark_title": "সবসময় কালো থিব ব্যাবহার",
"notification_title": "নোটিফিকেশন চালু করুন",
"edit-paired-devices_title": "যুক্ত ডিভাইস সম্পাদনা করুন",
"join-public-room_title": "সাময়িক ভাবে পাবলিক রুমে জয়েন করুন",
"edit-share-mode": "সম্পাদনা",
"expand_title": "হেডার বোতামের সারিটি বড় করুন"
},
"instructions": {
"activate-share-mode-and-other-file": "আর একটি ফাইল যোগ করুন",
"activate-share-mode-shared-file": "পাঠানো ফাইল",
"no-peers-subtitle": "ডিভাইস প্রদর্শিত হতে নতুন ডিভাইস যুক্ত করুন অথবা পাবলিক রুমে জয়েন দিন",
"no-peers-title": "ফাইল পাঠানোর জন্য অন্যান্য ডিভাইসে পেয়ারড্রপ খুলুন",
"x-instructions_data-drop-bg": "প্রাপক নির্বাচন করতে ছেড়ে দিন",
"no-peers_data-drop-bg": "প্রাপক নির্বাচন ছেড়ে দিন",
"x-instructions_desktop": "ফাইল পাঠাতে ক্লিক করুন অথবা মেসেজ পাঠাতে ডানে চাপুন",
"x-instructions_mobile": "ফাইল পাঠাতে ক্লিক করুন অথবা বেশি চেপে মেসেজ পাঠান",
"x-instructions_data-drop-peer": "পিয়ারকে পাঠানোর জন্য রিলিজ করুন",
"x-instructions-share-mode_desktop": "পাঠাতে ক্লিক করুন",
"x-instructions-share-mode_mobile": "পাঠাতে ক্লিক করুন",
"activate-share-mode-base": "অন্য ডিভাইসে পাঠাতে পেয়ারড্রপ খুলুন",
"activate-share-mode-and-other-files-plural": "অন্য ফাইল যোগ করুন",
"activate-share-mode-shared-text": "পাঠানো টেক্সট",
"activate-share-mode-shared-files-plural": "পাঠানো ফাইল গুলো"
}
}

View File

@ -73,7 +73,7 @@
"copied-text": "Testua arbelera kopiatu da",
"online": "Berriro zaude linean",
"unfinished-transfers-warning": "Amaitu gabeko trukatzeak daude. Ziur PairDrop itxi nahi duzula?",
"selected-peer-left": "Falta diren hautatutako kideak",
"selected-peer-left": "Hautatutako kideak alde egin du",
"pairing-key-invalidated": "{{key}} gakoa baliogabetu da"
},
"dialogs": {

View File

@ -1,18 +1,18 @@
{
"footer": {
"webrtc": "(WebRTCが無効なため)",
"public-room-devices_title": "公開ルーム内のデバイスは、接続中のネットワークと関係なくアクセスできます。",
"public-room-devices_title": "公開ルーム内のデバイスは、別のネットワークからもアクセスできます。",
"display-name_data-placeholder": "読み込み中…",
"display-name_title": "デバイス名を変更する",
"traffic": "この通信は",
"paired-devices_title": "ペアリング済みデバイスであれば、接続中のネットワークに関わらずアクセスできます。",
"paired-devices_title": "ペアリング済みデバイスは、別のネットワークからもアクセスできます。",
"public-room-devices": "ルーム{{roomId}}",
"paired-devices": "ペアリング済みデバイス",
"on-this-network": "このネットワーク",
"on-this-network": "このネットワーク",
"routed": "サーバーを経由します",
"discovery": "このデバイスを検出可能なネットワーク:",
"on-this-network_title": "このネットワークのすべてのデバイスからアクセスできます。",
"known-as": "他のデバイスに表示される名前:"
"on-this-network_title": "このネットワークのすべてのデバイスからアクセスできます。",
"known-as": "このデバイスの名前:"
},
"notifications": {
"request-title": "{{name}}は{{count}}個の{{descriptor}}を共有しようとしています",
@ -20,12 +20,12 @@
"message-received": "{{name}}から受信したメッセージ(クリックしてコピー)",
"rate-limit-join-key": "レート制限に到達しました。10秒待ってから再度お試しください。",
"connecting": "接続中…",
"pairing-key-invalidated": "コード{{key}}が失効しました",
"pairing-key-invalidated": "コード{{key}}は無効になりました",
"pairing-key-invalid": "無効なコード",
"connected": "接続しました",
"connected": "接続済み",
"pairing-not-persistent": "このデバイスとのペアリングは解除される可能性があります",
"text-content-incorrect": "無効なテキスト内容です",
"message-transfer-completed": "メッセージの送信が完了しました",
"message-transfer-completed": "メッセージを送信しました",
"file-transfer-completed": "ファイル転送が完了しました",
"file-content-incorrect": "無効なファイル内容です",
"files-incorrect": "ファイルが間違っています",
@ -39,7 +39,7 @@
"copied-to-clipboard-error": "コピーできませんでした。手動でコピーしてください。",
"pairing-success": "ペアリングしました",
"clipboard-content-incorrect": "無効なクリップボード内容です",
"display-name-changed-temporarily": "この接続のみデバイス名が変更されました",
"display-name-changed-temporarily": "この接続のみデバイス名が変更されました",
"copied-to-clipboard": "クリップボードにコピーしました",
"offline": "オフラインです",
"pairing-tabs-error": "同じWebブラウザーで開いたタブ同士でペアリングすることはできません",
@ -59,7 +59,7 @@
},
"header": {
"cancel-share-mode": "キャンセル",
"theme-auto_title": "システムテーマに合わせる",
"theme-auto_title": "システムテーマに合わせる",
"install_title": "PairDropをインストール",
"theme-dark_title": "常にダークテーマを使用する",
"pair-device_title": "他のデバイスとペアリングする",
@ -74,15 +74,15 @@
"expand_title": "ヘッダーボタン列を拡大する"
},
"instructions": {
"x-instructions_mobile": "タップでファイル送信、長押しでメッセージ送信します",
"x-instructions_mobile": "タップでファイル送信、長押しでメッセージ送信",
"x-instructions-share-mode_desktop": "クリックして{{descriptor}}を送信",
"activate-share-mode-and-other-files-plural": "とその他{{count}}個のファイル",
"x-instructions-share-mode_mobile": "タップして{{descriptor}}を送信",
"activate-share-mode-base": "他のデバイスでPairDropを開いて送信します",
"no-peers-subtitle": "ペアリングや公開ルームを使用すると、他のネットワーク上のデバイスと共有できます",
"no-peers-subtitle": "ペアリングや公開ルームを使うと、別のネットワークにあるデバイスと共有できます",
"activate-share-mode-shared-text": "共有されたテキスト",
"x-instructions_desktop": "左クリックでファイル送信、右クリックでメッセージ送信します",
"no-peers-title": "ファイル共有するには他のデバイスでPairDropを開いてください",
"x-instructions_desktop": "左クリックでファイル送信、右クリックでメッセージ送信",
"no-peers-title": "ファイル共有するには他のデバイスでPairDropを開きます",
"x-instructions_data-drop-peer": "ドロップするとこのデバイスに送信します",
"x-instructions_data-drop-bg": "送信したいデバイスの上でドロップしてください",
"no-peers_data-drop-bg": "送信したいデバイスの上でドロップしてください",
@ -94,7 +94,7 @@
"peer-ui": {
"processing": "処理中…",
"click-to-send-share-mode": "クリックして{{descriptor}}を送信",
"click-to-send": "左クリックでファイル送信、右クリックでメッセージ送信します",
"click-to-send": "左クリックでファイル送信、右クリックでメッセージ送信",
"waiting": "待機中…",
"connection-hash": "エンドツーエンド暗号化のセキュリティを確認するには、両方のデバイスのセキュリティナンバーを確認してください",
"preparing": "準備中…",
@ -102,8 +102,8 @@
},
"dialogs": {
"base64-paste-to-send": "ここをタップして{{type}}を送信",
"auto-accept-instructions-2": "」を有効にすると、そのデバイスから送信されたすべてのファイルを自動的に受け入れます。",
"receive-text-title": "メッセージを受信しました",
"auto-accept-instructions-2": "」が有効なら、そのデバイスが送信したすべてのファイルを自動で受け入れます。",
"receive-text-title": "メッセージを受信",
"edit-paired-devices-title": "ペアリング設定",
"cancel": "キャンセル",
"auto-accept-instructions-1": "「",
@ -123,19 +123,19 @@
"file-other-description-image": "とその他1個の画像",
"temporary-public-room-title": "公開ルーム",
"base64-files": "ファイル",
"has-sent": "が送信しました:",
"has-sent": "が送信:",
"file-other-description-file": "とその他1個のファイル",
"close": "閉じる",
"system-language": "システム言語",
"system-language": "システム言語",
"unpair": "ペアリング解除",
"title-image": "画像",
"file-other-description-file-plural": "とその他{{count}}個のファイル",
"would-like-to-share": "が以下のファイルを共有しようとしています",
"would-like-to-share": "がこれを共有しています",
"send-message-to": "このデバイスにメッセージを送信:",
"language-selector-title": "言語設定",
"pair": "ペアリング",
"hr-or": "または",
"scan-qr-code": "もしくはQRコードをスキャンしてください。",
"scan-qr-code": "QRコードをスキャンしてください。",
"input-key-on-this-device": "このコードを他のデバイスに入力するか",
"download-again": "もう一度ダウンロードする",
"accept": "承諾",
@ -162,20 +162,20 @@
"share-text-title": "テキストメッセージを共有します"
},
"about": {
"claim": "デバイス間のファイル共有を手軽に実現します",
"tweet_title": "PairDropのことをポストする",
"claim": "デバイス間でかんたんファイル共有",
"tweet_title": "PairDropについてポスト",
"close-about_aria-label": "PairDropについてを閉じる",
"buy-me-a-coffee_title": "コーヒーを一杯おごってください!",
"github_title": "PairDrop on GitHub",
"github_title": "GitHub上のPairDropプロジェクト",
"faq_title": "FAQ",
"mastodon_title": "MastodonにPairDropのことをトゥートする",
"mastodon_title": "MastodonでPairDropについてトゥート",
"bluesky_title": "BlueSkyでフォロー",
"custom_title": "フォロー",
"privacypolicy_title": "プライバシーポリシーを開く"
},
"document-titles": {
"file-transfer-requested": "ファイルの転送がリクエストされました",
"image-transfer-requested": "画像の転送がリクエストされました",
"file-transfer-requested": "ファイル転送の要求があります",
"image-transfer-requested": "画像の転送の要求があります",
"message-received-plural": "{{count}}個のメッセージを受信しました",
"message-received": "メッセージを受信しました",
"file-received": "ファイルを受信しました",

View File

@ -3,50 +3,62 @@
"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-auto_title": "Juster drakt til system automatisk",
"theme-light_title": "Alltid bruk lys drakt",
"theme-dark_title": "Alltid bruk mørk drakt",
"notification_title": "Skru på varslinger",
"cancel-share-mode": "Ferdig",
"install_title": "Installer PairDrop",
"pair-device_title": "Sammenkoble enhet",
"language-selector_title": "Velg språk"
"pair-device_title": "Sammenkoble dine enheter permanent",
"language-selector_title": "Velg språk",
"edit-share-mode": "Rediger",
"expand_title": "Utvid overskriftknapprad",
"join-public-room_title": "Bli med i et offentlig rom midlertidig"
},
"footer": {
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
"display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"display-name_title": "Rediger ditt enhetsnavn permanent",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
"known-as": "Du er kjent som:",
"paired-devices": "sammenkoblede enheter",
"routed": "Sendes gjennom tjeneren"
"paired-devices": "av sammenkoblede enheter",
"routed": "Sendes gjennom tjeneren",
"discovery": "Du kan bli oppdaget:",
"on-this-network_title": "Du kan bli oppdaget av alle på dette nettverket.",
"paired-devices_title": "Du kan alltid bli oppdaget av sammenkoblede enheter uavhengig av nettverk.",
"public-room-devices_title": "Du kan bli oppdaget av enheter i dette offentlige rommet uavhengig av nettverk.",
"public-room-devices": "i rom {{roomId}}"
},
"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",
"x-instructions-share-mode_desktop": "Klikk for å sende",
"x-instructions-share-mode_desktop": "Klikk for å sende {{descriptor}}",
"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",
"no-peers-subtitle": "Sammenkoble enheter eller bli med i et offentlig rom for å kunne oppdages på andre nettverk",
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
"x-instructions-share-mode_mobile": "Trykk for å sende",
"x-instructions-share-mode_mobile": "Trykk for å sende {{descriptor}}",
"activate-share-mode-base": "Åpne PairDrop på andre enheter for å sende",
"activate-share-mode-and-other-files-plural": "og {{count}} andre filer",
"activate-share-mode-shared-text": "delt tekst"
"activate-share-mode-shared-text": "delt tekst",
"activate-share-mode-and-other-file": "og 1 annen fil",
"activate-share-mode-shared-file": "delt fil",
"activate-share-mode-shared-files-plural": "{{count}} delte filer",
"webrtc-requirement": "For å bruke denne PairDrop-økten, må WebRTC være aktivert!"
},
"dialogs": {
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
"pair-devices-title": "Sammenkoble enheter",
"pair-devices-title": "Sammenkoble Enheter Permanent",
"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",
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet her.",
"edit-paired-devices-title": "Rediger Sammenkoblede Enheter",
"accept": "Godta",
"has-sent": "har sendt:",
"base64-paste-to-send": "Trykk her for å sende {{type}}",
"base64-paste-to-send": "Lim inn her for å dele {{type}}",
"base64-text": "tekst",
"base64-files": "filer",
"file-other-description-image-plural": "og {{count}} andre bilder",
@ -64,9 +76,9 @@
"receive-text-title": "Melding mottatt",
"auto-accept": "auto-godkjenn",
"share": "Del",
"send-message-to": "Send en melding til",
"send-message-to": "Til:",
"send": "Send",
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
"base64-tap-to-paste": "Trykk her for å dele {{type}}",
"file-other-description-image": "og ett annet bilde",
"file-other-description-file-plural": "og {{count}} andre filer",
"title-file-plural": "Filer",
@ -74,7 +86,28 @@
"file-other-description-file": "og én annen fil",
"title-image": "Bilde",
"title-file": "Fil",
"title-image-plural": "Bilder"
"title-image-plural": "Bilder",
"join": "Bli med",
"share-text-checkbox": "Alltid vis denne dialogen ved deling av tekst",
"language-selector-title": "Velg Språk",
"unpair": "Fjern sammenkobling",
"temporary-public-room-title": "Midlertidig Offentlig Rom",
"input-room-id-on-another-device": "Legg inn denne rom-IDen på en annen enhet",
"hr-or": "ELLER",
"leave": "Forlat",
"paired-device-removed": "Sammenkoblet enhet har blitt fjernet.",
"message_title": "Sett inn meldingen du vil sende",
"message_placeholder": "Tekst",
"base64-title-files": "Delte filer",
"system-language": "Systemspråk",
"public-room-qr-code_title": "Trykk for å kopiere lenke til offentlig rom",
"pair-devices-qr-code_title": "Trykk for å kopiere lenken til å sammenkoble denne enheten",
"approve": "godkjenn",
"share-text-title": "Del Tekstmelding",
"share-text-subtitle": "Rediger melding før sending:",
"close-toast_title": "Lukk varsel",
"enter-room-id-from-another-device": "Legg inn rom-ID fra en annen enhet for å bli med i rommet.",
"base64-title-text": "Delt Tekst"
},
"about": {
"close-about_aria-label": "Lukk «Om PairDrop»",
@ -82,7 +115,11 @@
"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"
"github_title": "PairDrop på GitHub",
"mastodon_title": "Skriv om PairDrop på Mastadon",
"bluesky_title": "Følg oss på BlueSky",
"custom_title": "Følg oss",
"privacypolicy_title": "Åpne vår personvernerklæring"
},
"notifications": {
"copied-to-clipboard": "Kopiert til utklippstavlen",
@ -95,7 +132,7 @@
"file-transfer-completed": "Filoverføring utført",
"selected-peer-left": "Valgt likemann dro",
"pairing-key-invalid": "Ugyldig nøkkel",
"connecting": "Kobler til …",
"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.",
@ -110,28 +147,37 @@
"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!",
"copied-text-error": "Kunne ikke legge innhold i utklippstavlen. 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."
"unfinished-transfers-warning": "Det er ufullførte overføringer. Er du sikker på at du vil lukke PairDrop?",
"rate-limit-join-key": "Grense nådd. Vent 10 sekunder og prøv igjen.",
"copied-to-clipboard-error": "Kopiering ikke mulig, Kopier manuelt.",
"public-room-id-invalid": "Ugyldig rom-ID",
"public-room-left": "Forlot offentlig rom {{publicRoomId}}",
"room-url-copied-to-clipboard": "Lenke for offentlig rom kopiert til utklippstavle",
"online-requirement-pairing": "Du må være på nett for å sammenkoble enheter",
"online-requirement-public-room": "Du må være på nett for å opprette et offentlig rom",
"pair-url-copied-to-clipboard": "Lenke for sammenkobling til denne enheten kopiert til utklipstavle",
"notifications-permissions-error": "Varlseltillatelse har blitt blokkert fordi brukeren har avvist forespørselen flere ganger. Dette kan tilbakestilles i Sideinnformasjon som kan bli funnet ved å trykke på låseikonet ved siden av URL-feltet."
},
"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"
"message-received-plural": "{{count}} meldinger mottatt",
"image-transfer-requested": "Blideoverføring forespurt"
},
"peer-ui": {
"preparing": "Forbereder …",
"preparing": "Forbereder…",
"waiting": "Venter…",
"processing": "Behandler …",
"transferring": "Overfører …",
"processing": "Behandler…",
"transferring": "Overfører…",
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
"click-to-send-share-mode": "Klikk for å sende {{descriptor}}",
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen"

View File

@ -26,7 +26,8 @@
}
],
"background_color": "#efefef",
"display": "minimal-ui",
"start_url": "./",
"display": "standalone",
"theme_color": "#3367d6",
"screenshots" : [
{

View File

@ -1,5 +1,7 @@
class BrowserTabsConnector {
constructor() {
if (!('BroadcastChannel' in window)) return;
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));

View File

@ -5,7 +5,7 @@ class Localization {
Localization.defaultLocale = "en";
Localization.supportedLocales = [
"ar", "be", "bg", "ca", "cs", "da", "de", "en", "es", "et", "eu", "fa", "fr", "he", "hu", "id", "it", "ja",
"kn", "ko", "nb", "nn", "nl", "pl", "pt-BR", "ro", "ru", "sk", "ta", "tr", "uk", "zh-CN", "zh-HK", "zh-TW"
"kn", "ko", "nb", "nl", "nn", "pl", "pt-BR", "ro", "ru", "sk", "ta", "tr", "uk", "zh-CN", "zh-HK", "zh-TW"
];
Localization.supportedLocalesRtl = ["ar", "he"];

View File

@ -14,10 +14,10 @@ class PairDrop {
"scripts/util.js",
"scripts/network.js",
"scripts/ui.js",
"scripts/qr-code.min.js",
"scripts/zip.min.js",
"scripts/no-sleep.min.js",
"scripts/heic2any.min.js"
"scripts/libs/heic2any.min.js",
"scripts/libs/no-sleep.min.js",
"scripts/libs/qr-code.min.js",
"scripts/libs/zip.min.js"
];
this.registerServiceWorker();
@ -81,7 +81,7 @@ class PairDrop {
}
onPwaInstallable(e) {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
if (!window.matchMedia('(display-mode: standalone)').matches) {
// only display install btn when not installed
this.$headerInstallBtn.removeAttribute('hidden');
this.$headerInstallBtn.addEventListener('click', () => {

View File

@ -1082,7 +1082,7 @@ class PeersManager {
}
async _onFilesSelected(message) {
let files = await mime.addMissingMimeTypesToFiles(message.files);
let files = mime.addMissingMimeTypesToFiles([...message.files]);
await this.peers[message.to].requestFileTransfer(files);
}
@ -1306,9 +1306,8 @@ class FileDigester {
const blob = new Blob(this._buffer)
this._buffer = null;
this._callback(new File([blob], this._name, {
type: this._mime,
type: this._mime || "application/octet-stream",
lastModified: new Date().getTime()
}));
}
}

View File

@ -333,65 +333,234 @@ class FooterUI {
class BackgroundCanvas {
constructor() {
this.c = $$('canvas');
this.cCtx = this.c.getContext('2d');
this.$canvas = $$('canvas');
this.$footer = $$('footer');
// redraw canvas
Events.on('resize', _ => this.init());
Events.on('redraw-canvas', _ => this.init());
Events.on('translation-loaded', _ => this.init());
// ShareMode
Events.on('share-mode-changed', e => this.onShareModeChanged(e.detail.active));
this.initAnimation();
}
async fadeIn() {
this.c.classList.remove('opacity-0');
this.$canvas.classList.remove('opacity-0');
}
init() {
let oldW = this.w;
let oldH = this.h;
let oldOffset = this.offset
this.w = document.documentElement.clientWidth;
this.h = document.documentElement.clientHeight;
this.offset = this.$footer.offsetHeight - 27;
if (this.h >= 800) this.offset += 10;
initAnimation() {
this.baseColorNormal = '168 168 168';
this.baseColorShareMode = '168 168 255';
this.baseOpacityNormal = 0.3;
this.baseOpacityShareMode = 0.8;
this.speed = 0.5;
this.fps = 60;
if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed
// if browser supports OffscreenCanvas
// -> put canvas drawing into serviceworker to unblock main thread
// otherwise
// -> use main thread
let {init, startAnimation, switchAnimation, onShareModeChange} =
this.$canvas.transferControlToOffscreen
? this.initAnimationOffscreen()
: this.initAnimationOnscreen();
this.c.width = this.w;
this.c.height = this.h;
this.x0 = this.w / 2;
this.y0 = this.h - this.offset;
this.dw = Math.round(Math.max(this.w, this.h, 1000) / 13);
this.baseColor = '165, 165, 165';
this.baseOpacity = 0.3;
init();
startAnimation();
this.drawCircles(this.cCtx);
// redraw canvas
Events.on('resize', _ => init());
Events.on('redraw-canvas', _ => init());
Events.on('translation-loaded', _ => init());
// ShareMode
Events.on('share-mode-changed', e => onShareModeChange(e.detail.active));
// Start and stop animation
Events.on('background-animation', e => switchAnimation(e.detail.animate))
Events.on('offline', _ => switchAnimation(false));
Events.on('online', _ => switchAnimation(true));
}
onShareModeChanged(active) {
this.baseColor = active ? '165, 165, 255' : '165, 165, 165';
this.baseOpacity = active ? 0.5 : 0.3;
this.drawCircles(this.cCtx);
initAnimationOnscreen() {
let $canvas = this.$canvas;
let $footer = this.$footer;
let baseColorNormal = this.baseColorNormal;
let baseColorShareMode = this.baseColorShareMode;
let baseOpacityNormal = this.baseOpacityNormal;
let baseOpacityShareMode = this.baseOpacityShareMode;
let speed = this.speed;
let fps = this.fps;
let c;
let cCtx;
let x0, y0, w, h, dw, offset;
let startTime;
let animate = true;
let currentFrame = 0;
let lastFrame;
let baseColor;
let baseOpacity;
function createCanvas() {
c = $canvas;
cCtx = c.getContext('2d');
lastFrame = fps / speed - 1;
baseColor = baseColorNormal;
baseOpacity = baseOpacityNormal;
}
function init() {
initCanvas($footer.offsetHeight, document.documentElement.clientWidth, document.documentElement.clientHeight);
}
drawCircle(ctx, radius) {
ctx.beginPath();
function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {
let oldW = w;
let oldH = h;
let oldOffset = offset;
w = clientWidth;
h = clientHeight;
offset = footerOffsetHeight - 28;
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
c.width = w;
c.height = h;
x0 = w / 2;
y0 = h - offset;
dw = Math.round(Math.min(Math.max(0.6 * w, h)) / 10);
drawFrame(currentFrame);
}
function startAnimation() {
startTime = Date.now();
animateBg();
}
function switchAnimation(state) {
if (!animate && state) {
// animation starts again. Set startTime to specific value to prevent frame jump
startTime = Date.now() - 1000 * currentFrame / fps;
}
animate = state;
requestAnimationFrame(animateBg);
}
function onShareModeChange(active) {
baseColor = active ? baseColorShareMode : baseColorNormal;
baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;
drawFrame(currentFrame);
}
function drawCircle(ctx, radius) {
ctx.lineWidth = 2;
let opacity = Math.max(0, this.baseOpacity * (1 - 1.2 * radius / Math.max(this.w, this.h)));
ctx.strokeStyle = `rgba(${this.baseColor}, ${opacity})`;
ctx.arc(this.x0, this.y0, radius, 0, 2 * Math.PI);
let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));
if (radius > dw * 7) {
opacity *= (8 * dw - radius) / dw
}
if (ctx.setStrokeColor) {
// older blink/webkit browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note
ctx.setStrokeColor("grey", opacity);
}
else {
ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;
}
ctx.beginPath();
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
ctx.stroke();
}
drawCircles(ctx) {
ctx.clearRect(0, 0, this.w, this.h);
for (let i = 0; i < 13; i++) {
this.drawCircle(ctx, this.dw * i + 33 + 66);
function drawCircles(ctx, frame) {
ctx.clearRect(0, 0, w, h);
for (let i = 7; i >= 0; i--) {
drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);
}
}
function drawFrame(frame) {
cCtx.clearRect(0, 0, w, h);
drawCircles(cCtx, frame);
}
function animateBg() {
let now = Date.now();
if (!animate && currentFrame === lastFrame) {
// Animation stopped and cycle finished -> stop drawing frames
return;
}
let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);
let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);
// Only draw frame if it differs from current frame
if (nextFrame !== currentFrame) {
drawFrame(nextFrame);
currentFrame = nextFrame;
}
requestAnimationFrame(animateBg);
}
createCanvas();
return {init, startAnimation, switchAnimation, onShareModeChange};
}
initAnimationOffscreen() {
console.log("Use OffscreenCanvas to draw background animation.")
let baseColorNormal = this.baseColorNormal;
let baseColorShareMode = this.baseColorShareMode;
let baseOpacityNormal = this.baseOpacityNormal;
let baseOpacityShareMode = this.baseOpacityShareMode;
let speed = this.speed;
let fps = this.fps;
let $canvas = this.$canvas;
let $footer = this.$footer;
const offscreen = $canvas.transferControlToOffscreen();
const worker = new Worker("scripts/worker/canvas-worker.js");
function createCanvas() {
worker.postMessage({
type: "createCanvas",
canvas: offscreen,
baseColorNormal: baseColorNormal,
baseColorShareMode: baseColorShareMode,
baseOpacityNormal: baseOpacityNormal,
baseOpacityShareMode: baseOpacityShareMode,
speed: speed,
fps: fps
}, [offscreen]);
}
function init() {
worker.postMessage({
type: "initCanvas",
footerOffsetHeight: $footer.offsetHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight
});
}
function startAnimation() {
worker.postMessage({ type: "startAnimation" });
}
function onShareModeChange(active) {
worker.postMessage({ type: "onShareModeChange", active: active });
}
function switchAnimation(animate) {
worker.postMessage({ type: "switchAnimation", animate: animate });
}
createCanvas();
return {init, startAnimation, switchAnimation, onShareModeChange};
}
}

View File

@ -18,11 +18,12 @@ class PeersUI {
this.peers = {};
this.shareMode = {};
this.shareMode.active = false;
this.shareMode.descriptor = "";
this.shareMode.files = [];
this.shareMode.text = "";
this.shareMode = {
active: false,
descriptor: "",
files: [],
text: ""
}
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-added', _ => this._evaluateOverflowingPeers());
@ -149,10 +150,17 @@ class PeersUI {
}
_onPeerDisconnected(peerId) {
// Remove peer from UI
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
this._evaluateOverflowingPeers();
// If no peer is shown -> start background animation again
if ($$('x-peers:empty')) {
Events.fire('background-animation', {animate: true});
}
}
_onRoomTypeRemoved(peerId, roomType) {
@ -178,10 +186,13 @@ class PeersUI {
this._onDragEnd();
if ($$('x-peer') || !$$('x-peer').contains(e.target)) return; // dropped on peer
if ($$('x-peer') && $$('x-peer').contains(e.target)) return; // dropped on peer
const files = e.dataTransfer.files;
const text = e.dataTransfer.getData("text");
let files = e.dataTransfer.files;
let text = e.dataTransfer.getData("text");
// convert FileList to Array
files = [...files];
if (files.length > 0) {
Events.fire('activate-share-mode', {
@ -214,8 +225,11 @@ class PeersUI {
if (this.shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault()
const files = e.clipboardData.files;
const text = e.clipboardData.getData("Text");
let files = e.clipboardData.files;
let text = e.clipboardData.getData("Text");
// convert FileList to Array
files = [...files];
if (files.length > 0) {
Events.fire('activate-share-mode', {files: files});
@ -277,8 +291,6 @@ class PeersUI {
descriptorInstructions = Localization.getTranslation("instructions.activate-share-mode-shared-file");
}
files = await mime.addMissingMimeTypesToFiles(files);
if (files[0].type.split('/')[0] === 'image') {
try {
let imageUrl = await getThumbnailAsDataUrl(files[0], 80, null, 0.9);
@ -394,12 +406,6 @@ class PeersUI {
class PeerUI {
static _badgeClassNames = ["badge-room-ip", "badge-room-secret", "badge-room-public-id"];
static _shareMode = {
active: false,
descriptor: ""
};
constructor(peer, connectionHash, shareMode) {
this.$xInstructions = $$('x-instructions');
this.$xPeers = $$('x-peers');
@ -409,7 +415,7 @@ class PeerUI {
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
// This is needed if the ShareMode is started BEFORE the PeerUI is drawn.
PeerUI._shareMode = shareMode;
this._shareMode = shareMode;
this._initDom();
@ -418,11 +424,14 @@ class PeerUI {
// ShareMode
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
// Stop background animation
Events.fire('background-animation', {animate: false});
}
html() {
let title= PeerUI._shareMode.active
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor})
let title= this._shareMode.active
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: this._shareMode.descriptor})
: Localization.getTranslation("peer-ui.click-to-send");
this.$el.innerHTML = `
@ -485,8 +494,8 @@ class PeerUI {
_onShareModeChanged(active = false, descriptor = "") {
// This is needed if the ShareMode is started AFTER the PeerUI is drawn.
PeerUI._shareMode.active = active;
PeerUI._shareMode.descriptor = descriptor;
this._shareMode.active = active;
this._shareMode.descriptor = descriptor;
this._evaluateShareMode();
this._bindListeners();
@ -494,12 +503,12 @@ class PeerUI {
_evaluateShareMode() {
let title;
if (!PeerUI._shareMode.active) {
if (!this._shareMode.active) {
title = Localization.getTranslation("peer-ui.click-to-send");
this.$input.removeAttribute('disabled');
}
else {
title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor});
title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: this._shareMode.descriptor});
this.$input.setAttribute('disabled', true);
}
this.$label.setAttribute('title', title);
@ -520,7 +529,7 @@ class PeerUI {
}
_bindListeners() {
if(!PeerUI._shareMode.active) {
if(!this._shareMode.active) {
// Remove Events Share mode
this.$el.removeEventListener('pointerdown', this._callbackPointerDown);
@ -636,7 +645,7 @@ class PeerUI {
}
_onDrop(e) {
if (PeerUI._shareMode.active || Dialog.anyDialogShown()) return;
if (this._shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
@ -864,7 +873,9 @@ class ReceiveDialog extends Dialog {
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
const fileExtension = fileNameSplit.length > 1
? '.' + fileNameSplit[fileNameSplit.length - 1]
: '';
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$fileSize.innerText = this._formatFileSize(totalSize);
@ -1974,7 +1985,7 @@ class SendTextDialog extends Dialog {
_onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.$peerDisplayName.classList.remove(...PeerUI._badgeClassNames);
this.$peerDisplayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
this.$peerDisplayName.classList.add($(peerId).ui._badgeClassName());
this.show();
@ -2056,7 +2067,7 @@ class ReceiveTextDialog extends Dialog {
_showReceiveTextDialog(text, peerId) {
this.$displayName.innerText = $(peerId).ui._displayName();
this.$displayName.classList.remove(...PeerUI._badgeClassNames);
this.$displayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
this.$displayName.classList.add($(peerId).ui._badgeClassName());
this.$text.innerText = text;
@ -2078,21 +2089,34 @@ class ReceiveTextDialog extends Dialog {
let m = 0;
const allowedDomainChars = "a-zA-Z0-9áàäčçđéèêŋńñóòôöšŧüžæøåëìíîïðùúýþćěłřśţźǎǐǒǔǥǧǩǯəʒâûœÿãõāēīōūăąĉċďĕėęĝğġģĥħĩĭįıĵķĸĺļľņňŏőŕŗŝşťũŭůűųŵŷżאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ";
const urlRgx = new RegExp(`(^|\\n|\\s|["><\\-_~:\\/?#\\[\\]@!$&'()*+,;=%.])(((https?:\\/\\/)?(?:[${allowedDomainChars}](?:[${allowedDomainChars}-]{0,61}[${allowedDomainChars}])?\\.)+[${allowedDomainChars}][${allowedDomainChars}-]{0,61}[${allowedDomainChars}])(:?\\d*)\\/?([${allowedDomainChars}_\\/\\-#.]*)(\\?([${allowedDomainChars}\\-_~:\\/?#\\[\\]@!$&'()*+,;=%.]*))?)`, 'g');
const chrs = `a-zA-Z0-9áàäčçđéèêŋńñóòôöšŧüžæøåëìíîïðùúýþćěłřśţźǎǐǒǔǥǧǩǯəʒâûœÿãõāēīōūăąĉċďĕėęĝğġģĥħĩĭįıĵķĸĺļľņňŏőŕŗŝşťũŭůűųŵŷżאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ`; // allowed chars in domain names
const rgxWhitespace = `(^|\\n|\\s)`;
const rgxScheme = `(https?:\\/\\/)`
const rgxSchemeMail = `(mailto:)`
const rgxUserinfo = `(?:(?:[${chrs}.%]*(?::[${chrs}.%]*)?)@)`;
const rgxHost = `(?:(?:[${chrs}](?:[${chrs}-]{0,61}[${chrs}])?\\.)+[${chrs}][${chrs}-]{0,61}[${chrs}])`;
const rgxPort = `(:\\d*)`;
const rgxPath = `(?:(?:\\/[${chrs}\\-\\._~!$&'\\(\\)\\*\\+,;=:@%]*)*)`;
const rgxQueryAndFragment = `(\\?[${chrs}\\-_~:\\/#\\[\\]@!$&'\\(\\)*+,;=%.]*)`;
const rgxUrl = `(${rgxScheme}?${rgxHost}${rgxPort}?${rgxPath}${rgxQueryAndFragment}?)`;
const rgxMail = `(${rgxSchemeMail}${rgxUserinfo}${rgxHost})`;
const rgxUrlAll = new RegExp(`${rgxWhitespace}${rgxUrl}`, 'g');
const rgxMailAll = new RegExp(`${rgxWhitespace}${rgxMail}`, 'g');
$textShadow.innerText = text.replace(urlRgx,
(match, whitespaceOrSpecial, url, g3, scheme) => {
const replaceMatchWithPlaceholder = function(match, whitespace, url, scheme) {
let link = url;
// prefix www.example.com with http protocol to prevent it from being a relative link
// prefix www.example.com with http scheme to prevent it from being a relative link
if (!scheme && link.startsWith('www')) {
link = "http://" + link
}
if (isUrlValid(link)) {
// link is valid -> replace with link node placeholder
if (!isUrlValid(link)) {
// link is not valid -> do not replace
return match;
}
// link is valid -> replace with link node placeholder
// find linkNodePlaceholder that is not yet present in text node
m++;
while (occP.includes(`${p}${m}`)) {
@ -2102,11 +2126,11 @@ class ReceiveTextDialog extends Dialog {
// add linkNodePlaceholder to text node and save a reference to linkNodes object
linkNodes[linkNodePlaceholder] = `<a href="${link}" target="_blank" rel="noreferrer">${url}</a>`;
return `${whitespaceOrSpecial}${linkNodePlaceholder}`;
return `${whitespace}${linkNodePlaceholder}`;
}
// link is not valid -> do not replace
return match;
});
text = text.replace(rgxUrlAll, replaceMatchWithPlaceholder);
$textShadow.innerText = text.replace(rgxMailAll, replaceMatchWithPlaceholder);
this.$text.innerHTML = $textShadow.innerHTML.replace(pRgx,
@ -2380,7 +2404,7 @@ class Base64Dialog extends Dialog {
class AboutUI {
constructor() {
this.$donationBtn = $('donation-btn');
this.$twitterBtn = $('twitter-btn');
this.$twitterBtn = $('x-twitter-btn');
this.$mastodonBtn = $('mastodon-btn');
this.$blueskyBtn = $('bluesky-btn');
this.$customBtn = $('custom-btn');

View File

@ -392,24 +392,25 @@ const mime = (() => {
"vob": "video/x-ms-vob",
"wmv": "video/x-ms-wmv",
"avi": "video/x-msvideo",
"*": "video/x-sgi-movie"
"*": "video/x-sgi-movie",
"kdbx": "application/x-keepass2"
}
return {
async guessMimeByFilename(filename) {
guessMimeByFilename(filename) {
const split = filename.split('.');
if (split.length === 1) {
// Filename does not include suffix
return "";
return false;
}
const suffix = split[split.length - 1].toLowerCase();
return suffixToMimeMap[suffix] || "";
return suffixToMimeMap[suffix];
},
async addMissingMimeTypesToFiles(files) {
addMissingMimeTypesToFiles(files) {
// if filetype is empty guess via suffix otherwise leave unchanged
for (let i = 0; i < files.length; i++) {
if (!files[i].type) {
files[i] = new File([files[i]], files[i].name, {type: await mime.guessMimeByFilename(files[i].name) || ""});
files[i] = new File([files[i]], files[i].name, {type: mime.guessMimeByFilename(files[i].name) || "application/octet-stream"});
}
}
return files;
@ -590,7 +591,7 @@ async function decodeBase64Text(base64) {
function isUrlValid(url) {
try {
let urlObj = new URL(url);
new URL(url);
return true;
}
catch (e) {

View File

@ -0,0 +1,141 @@
self.onmessage = (e) => {
switch (e.data.type) {
case "createCanvas": createCanvas(e.data);
break;
case "initCanvas": initCanvas(e.data.footerOffsetHeight, e.data.clientWidth, e.data.clientHeight);
break;
case "startAnimation": startAnimation();
break;
case "onShareModeChange": onShareModeChange(e.data.active);
break;
case "switchAnimation": switchAnimation(e.data.animate);
break;
}
};
let baseColorNormal;
let baseColorShareMode;
let baseOpacityNormal;
let baseOpacityShareMode;
let speed;
let fps;
let c;
let cCtx;
let x0, y0, w, h, dw, offset;
let startTime;
let animate = true;
let currentFrame = 0;
let lastFrame;
let baseColor;
let baseOpacity;
function createCanvas(data) {
baseColorNormal = data.baseColorNormal;
baseColorShareMode = data.baseColorShareMode;
baseOpacityNormal = data.baseOpacityNormal;
baseOpacityShareMode = data.baseOpacityShareMode;
speed = data.speed;
fps = data.fps;
c = data.canvas;
cCtx = c.getContext("2d");
lastFrame = fps / speed - 1;
baseColor = baseColorNormal;
baseOpacity = baseOpacityNormal;
}
function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {
let oldW = w;
let oldH = h;
let oldOffset = offset;
w = clientWidth;
h = clientHeight;
offset = footerOffsetHeight - 28;
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
c.width = w;
c.height = h;
x0 = w / 2;
y0 = h - offset;
dw = Math.round(Math.min(Math.max(0.6 * w, h)) / 10);
drawFrame(currentFrame);
}
function startAnimation() {
startTime = Date.now();
animateBg();
}
function switchAnimation(state) {
if (!animate && state) {
// animation starts again. Set startTime to specific value to prevent frame jump
startTime = Date.now() - 1000 * currentFrame / fps;
}
animate = state;
requestAnimationFrame(animateBg);
}
function onShareModeChange(active) {
baseColor = active ? baseColorShareMode : baseColorNormal;
baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;
drawFrame(currentFrame);
}
function drawCircle(ctx, radius) {
ctx.lineWidth = 2;
let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));
if (radius > dw * 7) {
opacity *= (8 * dw - radius) / dw
}
if (ctx.setStrokeColor) {
// older blink/webkit based browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor instead
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note
ctx.setStrokeColor("grey", opacity);
}
else {
ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;
}
ctx.beginPath();
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
ctx.stroke();
}
function drawCircles(ctx, frame) {
ctx.clearRect(0, 0, w, h);
for (let i = 7; i >= 0; i--) {
drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);
}
}
function drawFrame(frame) {
cCtx.clearRect(0, 0, w, h);
drawCircles(cCtx, frame);
}
function animateBg() {
let now = Date.now();
if (!animate && currentFrame === lastFrame) {
// Animation stopped and cycle finished -> stop drawing frames
return;
}
let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);
let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);
// Only draw frame if it differs from current frame
if (nextFrame !== currentFrame) {
drawFrame(nextFrame);
currentFrame = nextFrame;
}
requestAnimationFrame(animateBg);
}

View File

@ -1,22 +1,24 @@
const cacheVersion = 'v1.10.11';
const cacheVersion = 'v1.11.2';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
const relativePathsToCache = [
'./',
'index.html',
'manifest.json',
'styles/styles-main.css',
'styles/styles-deferred.css',
'scripts/browser-tabs-connector.js',
'scripts/localization.js',
'scripts/main.js',
'scripts/network.js',
'scripts/no-sleep.min.js',
'scripts/persistent-storage.js',
'scripts/qr-code.min.js',
'scripts/ui.js',
'scripts/ui-main.js',
'scripts/util.js',
'scripts/zip.min.js',
'scripts/worker/canvas-worker.js',
'scripts/libs/heic2any.min.js',
'scripts/libs/no-sleep.min.js',
'scripts/libs/qr-code.min.js',
'scripts/libs/zip.min.js',
'sounds/blop.mp3',
'sounds/blop.ogg',
'images/favicon-96x96.png',
@ -26,14 +28,19 @@ const relativePathsToCache = [
'images/android-chrome-512x512.png',
'images/android-chrome-512x512-maskable.png',
'images/apple-touch-icon.png',
'fonts/OpenSans/static/OpenSans-Medium.ttf',
'lang/ar.json',
'lang/be.json',
'lang/bg.json',
'lang/ca.json',
'lang/cs.json',
'lang/da.json',
'lang/de.json',
'lang/en.json',
'lang/es.json',
'lang/et.json',
'lang/eu.json',
'lang/fa.json',
'lang/fr.json',
'lang/he.json',
'lang/hu.json',
@ -41,15 +48,20 @@ const relativePathsToCache = [
'lang/it.json',
'lang/ja.json',
'lang/kn.json',
'lang/ko.json',
'lang/nb.json',
'lang/nl.json',
'lang/nn.json',
'lang/pl.json',
'lang/pt-BR.json',
'lang/ro.json',
'lang/ru.json',
'lang/sk.json',
'lang/ta.json',
'lang/tr.json',
'lang/uk.json',
'lang/zh-CN.json',
'lang/zh-HK.json',
'lang/zh-TW.json'
];
const relativePathsNotToCache = [
@ -58,13 +70,15 @@ const relativePathsNotToCache = [
self.addEventListener('install', function(event) {
// Perform install steps
console.log("Cache files for sw:", cacheVersion);
event.waitUntil(
caches.open(cacheTitle)
.then(function(cache) {
return cache
.addAll(relativePathsToCache)
.then(_ => {
console.log('All files cached.');
console.log('All files cached for sw:', cacheVersion);
self.skipWaiting();
});
})
);
@ -74,20 +88,25 @@ self.addEventListener('install', function(event) {
const fromNetwork = (request, timeout) =>
new Promise((resolve, reject) => {
const timeoutId = setTimeout(reject, timeout);
fetch(request)
fetch(request, {cache: "no-store"})
.then(response => {
if (response.redirected) {
throw new Error("Fetch is redirect. Abort usage and cache!");
}
clearTimeout(timeoutId);
resolve(response);
// Prevent requests that are in relativePathsNotToCache from being cached
if (doNotCacheRequest(request)) return;
update(request)
updateCache(request)
.then(() => console.log("Cache successfully updated for", request.url))
.catch(reason => console.log("Cache could not be updated for", request.url, "Reason:", reason));
.catch(err => console.log("Cache could not be updated for", request.url, err));
})
.catch(error => {
// Handle any errors that occurred during the fetch
console.error(`Could not fetch ${request.url}. Are you online?`);
console.error(`Could not fetch ${request.url}.`);
reject(error);
});
});
@ -109,16 +128,16 @@ const doNotCacheRequest = request => {
};
// cache the current page to make it available for offline
const update = request => new Promise((resolve, reject) => {
if (doNotCacheRequest(request)) {
reject("Url is specifically prevented from being cached in the serviceworker.");
return;
}
const updateCache = request => new Promise((resolve, reject) => {
caches
.open(cacheTitle)
.then(cache =>
fetch(request, {cache: "no-store"})
.then(response => {
if (response.redirected) {
throw new Error("Fetch is redirect. Abort usage and cache!");
}
cache
.put(request, response)
.then(() => resolve());
@ -127,11 +146,19 @@ const update = request => new Promise((resolve, reject) => {
);
});
// general strategy when making a request (eg if online try to fetch it
// 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
// general strategy when making a request:
// 1. Try to retrieve file from cache
// 2. If cache is not available: Fetch from network and update cache.
// This way, cached files are only updated if the cacheVersion is changed
self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
const swOrigin = new URL(self.location.href).origin;
const requestOrigin = new URL(event.request.url).origin;
if (swOrigin !== requestOrigin) {
// Do not handle requests from other origin
event.respondWith(fetch(event.request));
}
else if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
const share_url = await evaluateRequestData(event.request);
@ -139,39 +166,48 @@ self.addEventListener('fetch', function(event) {
})());
}
else {
// Regular requests not related to Web Share Target.
if (forceFetch) {
event.respondWith(fromNetwork(event.request, 10000));
}
else {
// Regular requests not related to Web Share Target:
// If request is excluded from cache -> respondWith fromNetwork
// else -> try fromCache first
event.respondWith(
fromCache(event.request)
doNotCacheRequest(event.request)
? fromNetwork(event.request, 10000)
: fromCache(event.request)
.then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
if (!rsp) {
throw new Error("No match found.");
}
return rsp;
})
.catch(error => {
console.error("Could not retrieve request from cache:", event.request.url, error);
return fromNetwork(event.request, 10000);
})
);
}
}
});
// on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt => {
console.log("Activate sw:", cacheVersion);
evt.waitUntil(clients.claim());
return evt.waitUntil(
caches.keys()
caches
.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
console.log("Delete cache:", cacheName);
return caches.delete(cacheName);
}
})
);
})
)
}
);
});
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {

View File

@ -11,10 +11,6 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
transition: color 300ms;
}
@ -591,7 +587,6 @@ x-dialog:not([show]) x-background {
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
user-select: none;
background: inherit;
@ -696,7 +691,6 @@ button::-moz-focus-inner {
/* Info Animation */
#about {
color: white;
z-index: 32;
@ -752,9 +746,11 @@ button::-moz-focus-inner {
height: var(--size);
z-index: -1;
background: var(--primary-color);
background-image: radial-gradient(circle at calc(50% - 36px), var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 40%, black) 80%);
background-image: radial-gradient(circle at calc(50% - 36px), var(--primary-color) 0%, black 80%);
--crop-size: 0px;
clip-path: circle(var(--crop-size));
/* For clients < iOS 13.1 */
-webkit-clip-path: circle(var(--crop-size));
}
html:not([dir="rtl"]) #about x-background {
@ -902,7 +898,7 @@ x-peers:empty~x-instructions {
@media screen and (min-height: 800px) {
footer {
margin-bottom: 16px;
padding-bottom: 10px;
}
}
@ -912,6 +908,13 @@ x-peers:empty~x-instructions {
}
}
/* PWA Standalone styles */
@media all and (display-mode: standalone) {
footer {
padding-bottom: 34px;
}
}
/* Constants */
:root {