Compare commits

..

No commits in common. "master" and "v1.10.11" have entirely different histories.

25 changed files with 259 additions and 721 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.11.2
Version: v1.10.11
**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.11.2
Version: v1.10.11
**Additional context**
Add any other context about the problem here.

View File

@ -110,7 +110,6 @@ 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.11.2/pairdrop-cli.zip"
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/pairdrop-cli.zip"
```
or
```shell
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip"
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/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.11.2",
"version": "1.10.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.11.2",
"version": "1.10.11",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
@ -68,9 +68,9 @@
}
},
"node_modules/call-bind-apply-helpers": {
"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==",
"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==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -263,9 +263,9 @@
}
},
"node_modules/express-rate-limit": {
"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==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz",
"integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==",
"engines": {
"node": ">= 16"
},
@ -273,7 +273,7 @@
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": "^4.11 || 5 || ^5.0.0-beta.1"
"express": "4 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/finalhandler": {
@ -318,16 +318,16 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"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==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"call-bind-apply-helpers": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"get-proto": "^1.0.0",
"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.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
"integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
"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==",
"funding": [
{
"type": "opencollective",
@ -775,9 +775,6 @@
"url": "https://github.com/sponsors/faisalman"
}
],
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
@ -815,9 +812,9 @@
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},

View File

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

View File

@ -10,7 +10,6 @@
<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 -->
@ -417,12 +416,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="row center p-2">
<div class="font-subheading">
<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>
</div>
</p>
</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>
@ -668,7 +667,7 @@
</svg>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.11.2</div>
<div class="font-subheading">v1.10.11</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
@ -682,9 +681,9 @@
<use xlink:href="#donation"></use>
</svg>
</a>
<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">
<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">
<svg class="icon">
<use xlink:href="#x-twitter"></use>
<use xlink:href="#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>
@ -740,8 +739,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="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 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>
<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>
@ -811,9 +810,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 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 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>
<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.-->

View File

@ -1,35 +0,0 @@
{
"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": "Hautatutako kideak alde egin du",
"selected-peer-left": "Falta diren hautatutako kideak",
"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": "GitHub上のPairDropプロジェクト",
"github_title": "PairDrop on GitHub",
"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,62 +3,50 @@
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
"about_title": "Om PairDrop",
"about_aria-label": "Åpne «Om PairDrop»",
"theme-auto_title": "Juster drakt til system automatisk",
"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å varslinger",
"cancel-share-mode": "Ferdig",
"install_title": "Installer PairDrop",
"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"
"pair-device_title": "Sammenkoble enhet",
"language-selector_title": "Velg språk"
},
"footer": {
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
"display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger ditt enhetsnavn permanent",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
"known-as": "Du er kjent som:",
"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}}"
"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",
"x-instructions-share-mode_desktop": "Klikk for å sende {{descriptor}}",
"x-instructions-share-mode_desktop": "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 eller bli med i et offentlig rom for å kunne oppdages på andre nettverk",
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
"x-instructions-share-mode_mobile": "Trykk for å sende {{descriptor}}",
"x-instructions-share-mode_mobile": "Trykk for å sende",
"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-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!"
"activate-share-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 Permanent",
"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 her.",
"edit-paired-devices-title": "Rediger 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": "Lim inn her for å dele {{type}}",
"base64-paste-to-send": "Trykk her for å sende {{type}}",
"base64-text": "tekst",
"base64-files": "filer",
"file-other-description-image-plural": "og {{count}} andre bilder",
@ -76,9 +64,9 @@
"receive-text-title": "Melding mottatt",
"auto-accept": "auto-godkjenn",
"share": "Del",
"send-message-to": "Til:",
"send-message-to": "Send en melding til",
"send": "Send",
"base64-tap-to-paste": "Trykk her for å dele {{type}}",
"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",
@ -86,28 +74,7 @@
"file-other-description-file": "og én annen fil",
"title-image": "Bilde",
"title-file": "Fil",
"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"
"title-image-plural": "Bilder"
},
"about": {
"close-about_aria-label": "Lukk «Om PairDrop»",
@ -115,11 +82,7 @@
"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",
"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"
"github_title": "PairDrop på GitHub"
},
"notifications": {
"copied-to-clipboard": "Kopiert til utklippstavlen",
@ -147,31 +110,22 @@
"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 utklippstavlen. Kopier manuelt!",
"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": "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."
"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",
"image-transfer-requested": "Blideoverføring forespurt"
"message-received-plural": "{{count}} meldinger mottatt"
},
"peer-ui": {
"preparing": "Forbereder …",

View File

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

View File

@ -1,7 +1,5 @@
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", "nl", "nn", "pl", "pt-BR", "ro", "ru", "sk", "ta", "tr", "uk", "zh-CN", "zh-HK", "zh-TW"
"kn", "ko", "nb", "nn", "nl", "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/libs/heic2any.min.js",
"scripts/libs/no-sleep.min.js",
"scripts/libs/qr-code.min.js",
"scripts/libs/zip.min.js"
"scripts/qr-code.min.js",
"scripts/zip.min.js",
"scripts/no-sleep.min.js",
"scripts/heic2any.min.js"
];
this.registerServiceWorker();
@ -81,7 +81,7 @@ class PairDrop {
}
onPwaInstallable(e) {
if (!window.matchMedia('(display-mode: standalone)').matches) {
if (!window.matchMedia('(display-mode: minimal-ui)').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 = mime.addMissingMimeTypesToFiles([...message.files]);
let files = await mime.addMissingMimeTypesToFiles(message.files);
await this.peers[message.to].requestFileTransfer(files);
}
@ -1306,8 +1306,9 @@ class FileDigester {
const blob = new Blob(this._buffer)
this._buffer = null;
this._callback(new File([blob], this._name, {
type: this._mime || "application/octet-stream",
type: this._mime,
lastModified: new Date().getTime()
}));
}
}

View File

@ -333,234 +333,65 @@ class FooterUI {
class BackgroundCanvas {
constructor() {
this.$canvas = $$('canvas');
this.c = $$('canvas');
this.cCtx = this.c.getContext('2d');
this.$footer = $$('footer');
this.initAnimation();
// 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));
}
async fadeIn() {
this.$canvas.classList.remove('opacity-0');
this.c.classList.remove('opacity-0');
}
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;
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;
// 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();
if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed
init();
startAnimation();
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;
// 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));
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;
onShareModeChanged(active) {
this.baseColor = active ? '165, 165, 255' : '165, 165, 165';
this.baseOpacity = active ? 0.5 : 0.3;
this.drawCircles(this.cCtx);
}
function init() {
initCanvas($footer.offsetHeight, document.documentElement.clientWidth, document.documentElement.clientHeight);
}
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 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})`;
}
drawCircle(ctx, radius) {
ctx.beginPath();
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
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);
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);
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 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,12 +18,11 @@ class PeersUI {
this.peers = {};
this.shareMode = {
active: false,
descriptor: "",
files: [],
text: ""
}
this.shareMode = {};
this.shareMode.active = false;
this.shareMode.descriptor = "";
this.shareMode.files = [];
this.shareMode.text = "";
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-added', _ => this._evaluateOverflowingPeers());
@ -150,17 +149,10 @@ 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) {
@ -186,13 +178,10 @@ 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
let files = e.dataTransfer.files;
let text = e.dataTransfer.getData("text");
// convert FileList to Array
files = [...files];
const files = e.dataTransfer.files;
const text = e.dataTransfer.getData("text");
if (files.length > 0) {
Events.fire('activate-share-mode', {
@ -225,11 +214,8 @@ class PeersUI {
if (this.shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault()
let files = e.clipboardData.files;
let text = e.clipboardData.getData("Text");
// convert FileList to Array
files = [...files];
const files = e.clipboardData.files;
const text = e.clipboardData.getData("Text");
if (files.length > 0) {
Events.fire('activate-share-mode', {files: files});
@ -291,6 +277,8 @@ 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);
@ -406,6 +394,12 @@ 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');
@ -415,7 +409,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.
this._shareMode = shareMode;
PeerUI._shareMode = shareMode;
this._initDom();
@ -424,14 +418,11 @@ 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= this._shareMode.active
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: this._shareMode.descriptor})
let title= PeerUI._shareMode.active
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor})
: Localization.getTranslation("peer-ui.click-to-send");
this.$el.innerHTML = `
@ -494,8 +485,8 @@ class PeerUI {
_onShareModeChanged(active = false, descriptor = "") {
// This is needed if the ShareMode is started AFTER the PeerUI is drawn.
this._shareMode.active = active;
this._shareMode.descriptor = descriptor;
PeerUI._shareMode.active = active;
PeerUI._shareMode.descriptor = descriptor;
this._evaluateShareMode();
this._bindListeners();
@ -503,12 +494,12 @@ class PeerUI {
_evaluateShareMode() {
let title;
if (!this._shareMode.active) {
if (!PeerUI._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: this._shareMode.descriptor});
title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor});
this.$input.setAttribute('disabled', true);
}
this.$label.setAttribute('title', title);
@ -529,7 +520,7 @@ class PeerUI {
}
_bindListeners() {
if(!this._shareMode.active) {
if(!PeerUI._shareMode.active) {
// Remove Events Share mode
this.$el.removeEventListener('pointerdown', this._callbackPointerDown);
@ -645,7 +636,7 @@ class PeerUI {
}
_onDrop(e) {
if (this._shareMode.active || Dialog.anyDialogShown()) return;
if (PeerUI._shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
@ -873,9 +864,7 @@ class ReceiveDialog extends Dialog {
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = fileNameSplit.length > 1
? '.' + fileNameSplit[fileNameSplit.length - 1]
: '';
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$fileSize.innerText = this._formatFileSize(totalSize);
@ -1985,7 +1974,7 @@ class SendTextDialog extends Dialog {
_onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.$peerDisplayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
this.$peerDisplayName.classList.remove(...PeerUI._badgeClassNames);
this.$peerDisplayName.classList.add($(peerId).ui._badgeClassName());
this.show();
@ -2067,7 +2056,7 @@ class ReceiveTextDialog extends Dialog {
_showReceiveTextDialog(text, peerId) {
this.$displayName.innerText = $(peerId).ui._displayName();
this.$displayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
this.$displayName.classList.remove(...PeerUI._badgeClassNames);
this.$displayName.classList.add($(peerId).ui._badgeClassName());
this.$text.innerText = text;
@ -2089,34 +2078,21 @@ class ReceiveTextDialog extends Dialog {
let m = 0;
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');
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 replaceMatchWithPlaceholder = function(match, whitespace, url, scheme) {
$textShadow.innerText = text.replace(urlRgx,
(match, whitespaceOrSpecial, url, g3, scheme) => {
let link = url;
// prefix www.example.com with http scheme to prevent it from being a relative link
// prefix www.example.com with http protocol to prevent it from being a relative link
if (!scheme && link.startsWith('www')) {
link = "http://" + link
}
if (!isUrlValid(link)) {
// link is not valid -> do not replace
return match;
}
if (isUrlValid(link)) {
// link is valid -> replace with link node placeholder
// find linkNodePlaceholder that is not yet present in text node
m++;
while (occP.includes(`${p}${m}`)) {
@ -2126,11 +2102,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 `${whitespace}${linkNodePlaceholder}`;
return `${whitespaceOrSpecial}${linkNodePlaceholder}`;
}
text = text.replace(rgxUrlAll, replaceMatchWithPlaceholder);
$textShadow.innerText = text.replace(rgxMailAll, replaceMatchWithPlaceholder);
// link is not valid -> do not replace
return match;
});
this.$text.innerHTML = $textShadow.innerHTML.replace(pRgx,
@ -2404,7 +2380,7 @@ class Base64Dialog extends Dialog {
class AboutUI {
constructor() {
this.$donationBtn = $('donation-btn');
this.$twitterBtn = $('x-twitter-btn');
this.$twitterBtn = $('twitter-btn');
this.$mastodonBtn = $('mastodon-btn');
this.$blueskyBtn = $('bluesky-btn');
this.$customBtn = $('custom-btn');

View File

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

View File

@ -1,141 +0,0 @@
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,24 +1,22 @@
const cacheVersion = 'v1.11.2';
const cacheVersion = 'v1.10.11';
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/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',
'scripts/zip.min.js',
'sounds/blop.mp3',
'sounds/blop.ogg',
'images/favicon-96x96.png',
@ -28,19 +26,14 @@ 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',
@ -48,20 +41,15 @@ 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 = [
@ -70,15 +58,13 @@ 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 for sw:', cacheVersion);
self.skipWaiting();
console.log('All files cached.');
});
})
);
@ -88,25 +74,20 @@ self.addEventListener('install', function(event) {
const fromNetwork = (request, timeout) =>
new Promise((resolve, reject) => {
const timeoutId = setTimeout(reject, timeout);
fetch(request, {cache: "no-store"})
fetch(request)
.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;
updateCache(request)
update(request)
.then(() => console.log("Cache successfully updated for", request.url))
.catch(err => console.log("Cache could not be updated for", request.url, err));
.catch(reason => console.log("Cache could not be updated for", request.url, "Reason:", reason));
})
.catch(error => {
// Handle any errors that occurred during the fetch
console.error(`Could not fetch ${request.url}.`);
console.error(`Could not fetch ${request.url}. Are you online?`);
reject(error);
});
});
@ -128,16 +109,16 @@ const doNotCacheRequest = request => {
};
// cache the current page to make it available for offline
const updateCache = request => new Promise((resolve, reject) => {
const update = request => new Promise((resolve, reject) => {
if (doNotCacheRequest(request)) {
reject("Url is specifically prevented from being cached in the serviceworker.");
return;
}
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());
@ -146,19 +127,11 @@ const updateCache = request => new Promise((resolve, reject) => {
);
});
// 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
// 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
self.addEventListener('fetch', function(event) {
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") {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
const share_url = await evaluateRequestData(event.request);
@ -166,48 +139,39 @@ self.addEventListener('fetch', function(event) {
})());
}
else {
// Regular requests not related to Web Share Target:
// If request is excluded from cache -> respondWith fromNetwork
// else -> try fromCache first
// Regular requests not related to Web Share Target.
if (forceFetch) {
event.respondWith(fromNetwork(event.request, 10000));
}
else {
event.respondWith(
doNotCacheRequest(event.request)
? fromNetwork(event.request, 10000)
: fromCache(event.request)
fromCache(event.request)
.then(rsp => {
// if fromCache resolves to undefined fetch from network instead
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);
return rsp || 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,6 +11,10 @@ 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;
}
@ -587,6 +591,7 @@ 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;
@ -691,6 +696,7 @@ button::-moz-focus-inner {
/* Info Animation */
#about {
color: white;
z-index: 32;
@ -746,11 +752,9 @@ button::-moz-focus-inner {
height: var(--size);
z-index: -1;
background: var(--primary-color);
background-image: radial-gradient(circle at calc(50% - 36px), var(--primary-color) 0%, black 80%);
background-image: radial-gradient(circle at calc(50% - 36px), var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 40%, 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 {
@ -898,7 +902,7 @@ x-peers:empty~x-instructions {
@media screen and (min-height: 800px) {
footer {
padding-bottom: 10px;
margin-bottom: 16px;
}
}
@ -908,13 +912,6 @@ x-peers:empty~x-instructions {
}
}
/* PWA Standalone styles */
@media all and (display-mode: standalone) {
footer {
padding-bottom: 34px;
}
}
/* Constants */
:root {