Compare commits

..

4 Commits

Author SHA1 Message Date
schlagmichdoch 06d00668b8 Increase WebRTC file transfer speed by implementing the bufferamountlow event properly 2023-10-13 18:50:17 +02:00
schlagmichdoch 49b749f32b Add speed log to browser console 2023-10-13 18:50:17 +02:00
schlagmichdoch 8054794a70 Fix byte size conversion 2023-10-13 18:50:17 +02:00
schlagmichdoch 702f0715a2 fix ws-fallback. Variable that indicates WebRTC support was always false 2023-10-13 18:50:17 +02:00
199 changed files with 16048 additions and 11208 deletions
+2 -10
View File
@@ -1,13 +1,5 @@
node_modules
.github .github
.git* .git*
.idea
dev
docs
licenses
node_modules
pairdrop-cli
*.md *.md
*.yml
Dockerfile
rtc_config_example.json
turnserver_example.conf
+3 -3
View File
@@ -36,15 +36,15 @@ If applicable, add screenshots to help explain your problem.
**Bug occurs on official PairDrop instance https://pairdrop.net/** **Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes No | Yes
Version: v1.10.10 Version: v1.9.1
**Bug occurs on self-hosted PairDrop instance** **Bug occurs on self-hosted PairDrop instance**
No | Yes No | Yes
**Self-Hosted Setup** **Self-Hosted Setup**
Proxy: Nginx | Apache2 Proxy: Nginx | Apache2
Deployment: docker run | docker compose | npm run start:prod Deployment: docker run | docker-compose | npm run start:prod
Version: v1.10.10 Version: v1.9.1
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
+1 -12
View File
@@ -1,14 +1,3 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
# Build a Docker image whenever it is pushed to master
name: Docker Image CI name: Docker Image CI
on: on:
@@ -24,6 +13,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@v3
- name: Build the Docker image - name: Build the Docker image
run: docker build --pull . -f Dockerfile -t pairdrop run: docker build --pull . -f Dockerfile -t pairdrop
+6 -8
View File
@@ -7,8 +7,6 @@
# To get a newer version, you will need to update the SHA. # To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning. # You can also reference a tag or branch, but the action may change without warning.
# Create a Docker image and push it to ghcr.io whenever a new version tag is pushed
name: GHCR Image CI name: GHCR Image CI
on: on:
@@ -29,16 +27,16 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@v3
- name: Setup qemu - name: Setup qemu
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 uses: docker/setup-qemu-action@v2.1.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 uses: docker/setup-buildx-action@v2.5.0
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -46,12 +44,12 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@31cebacef4805868f9ce9a0cb03ee36c32df2ac4 # v5.3.0 uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
-36
View File
@@ -1,36 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
# Create a new zip file from pairdrop-cli whenever a new version tag is pushed
name: Zip Release
on:
push:
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@master
- name: Archive Release
uses: thedoctor0/zip-release@b57d897cb5d60cb78b51a507f63fa184cfe35554 # v0.7.6
with:
filename: 'pairdrop-cli.zip'
directory: 'pairdrop-cli'
exclusions: '*.git* /*node_modules/* .editorconfig'
- name: Upload Release
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
with:
artifacts: "pairdrop-cli/pairdrop-cli.zip"
token: ${{ secrets.GITHUB_TOKEN }}
+2 -4
View File
@@ -1,7 +1,5 @@
node_modules node_modules
.DS_Store .DS_Store
/dev/certs fqdn.env
/docker/certs
qrcode-svg/ qrcode-svg/
turnserver.conf
rtc_config.json
ssl/
+2 -9
View File
@@ -1,21 +1,14 @@
FROM alpine:latest FROM node:lts-alpine
WORKDIR /home/node/app WORKDIR /home/node/app
COPY package*.json ./ COPY package*.json ./
RUN apk add --no-cache nodejs npm RUN npm ci
RUN NODE_ENV="production" npm ci --omit=dev
# Directories and files excluded via .dockerignore
COPY . . COPY . .
# environment settings
ENV NODE_ENV="production"
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1 CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1
ENTRYPOINT ["npm", "start"]
+85 -82
View File
@@ -1,128 +1,131 @@
<div align="center"> <div align="center">
<a href="https://github.com/schlagmichdoch/PairDrop"> <a href="https://github.com/schlagmichdoch/PairDrop">
<img src="public/images/android-chrome-512x512.png" alt="Logo" width="150" height="150"> <img src="https://raw.githubusercontent.com/schlagmichdoch/PairDrop/master/public/images/android-chrome-512x512.png" alt="Logo" width="150" height="150">
</a> </a>
# _Send it_, with [PairDrop](https://pairdrop.net) <h1>PairDrop</h1>
<p> <p>
Local file sharing <a href="https://pairdrop.net"><strong>in your web browser</strong></a>. Local file sharing in your browser. Inspired by Apple's AirDrop.
<br />
<a href="https://pairdrop.net"><strong>Explore »</strong></a>
<br /> <br />
<br /> <br />
<a href="https://github.com/schlagmichdoch/PairDrop/issues">Report a bug</a> <a href="https://github.com/schlagmichdoch/PairDrop/issues">Report Bug</a>
<br /> ·
<a href="https://github.com/schlagmichdoch/PairDrop/issues">Request feature</a> <a href="https://github.com/schlagmichdoch/PairDrop/issues">Request Feature</a>
</p> </p>
</div> </div>
## Features ## Features
File sharing on your local network that works on all platforms. [PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
- A multi-platform AirDrop-like solution that works. - File Sharing on your local network
- Send images, documents or text via peer-to-peer connection to devices on the same local network. - Send images, documents or text via peer to peer connection to devices on the same local network.
- Internet transfers - Internet Transfers
- Join temporary public rooms to transfer files easily over the Internet. - Join temporary public rooms to transfer files easily over the internet!
- Web-app - Web-Application
- Works on all devices with a modern web-browser. - As it is web based, it runs on all devices.
Send a file from your phone to your laptop? You want to quickly send a file from your phone to your laptop?
<br>Share photos in original quality with friends using Android and iOS? <br>You want to share photos in original quality with friends that use a mixture of Android and iOS?
<br>Share private files peer-to-peer between Linux systems? <br>You want to share private files peer to peer between Linux systems?
<br>AirDrop is unreliable again?
<br>_Send it with PairDrop!_
<img src="docs/pairdrop_screenshot_mobile.gif" alt="Screenshot GIF showing PairDrop in use" style="width: 300px"> Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
## Differences to the [Snapdrop](https://github.com/RobinLinus/snapdrop) it is based on ## Differences to Snapdrop
<details><summary>View all differences</summary> <details><summary>Click to expand</summary>
### Paired Devices and Public Rooms Internet Transfer ### Paired Devices and Public Rooms - Internet Transfer
* Transfer files over the Internet between paired devices or by entering temporary public rooms. * Transfer files over the internet between paired devices or by entering temporary public rooms.
* Connect to devices in complex network environments (public Wi-Fi, company network, iCloud Private Relay, VPN, etc.). * Connect to devices in complex network environments (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
* Connect to devices on your mobile hotspot. * Connect to devices on your mobile hotspot.
* Devices outside of your local network that are behind a NAT are auto-connected via the PairDrop TURN server. * Devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
* Devices from the local network, in the same public room, or previously paired are shown. * Connect to devices on your mobile hotspot.
* You will always discover devices on your local network. Paired devices and devices in the same public room are shown additionally.
#### Persistent Device Pairing #### Persistent Device Pairing
* Pair your devices via a 6-digit code or a QR-Code.
Always connect to known devices * Paired devices will always find each other via shared secrets independently of their local network.
* Paired devices are persistent. You find your devices even after reopening PairDrop.
* Pair devices via a 6-digit code or a QR-Code. * You can edit and unpair devices easily
* Paired devices always find each other via shared secrets independently of their local network. * Ideal to always connect easily to your own devices
* Pairing is persistent. You find your devices even after reopening PairDrop.
* You can edit and unpair devices easily.
#### Temporary Public Rooms #### Temporary Public Rooms
* Enter a public room via a 5-letter code or a QR-Code.
Connect to others in complex network situations, or over the Internet.
* Enter a public room via a 5-letter code or a QR-code.
* Enter a public room to temporarily connect to devices outside your local network. * Enter a public room to temporarily connect to devices outside your local network.
* All devices in the same public room see each other. * All devices in the same public room see each other mutually.
* Public rooms are temporary. Closing PairDrop leaves all rooms. * Public rooms are temporary. Public rooms are left as soon as PairDrop is closed.
* Ideal to connect easily to others in complex network situations or over the internet.
### [Improved UI for Sending/Receiving Files](https://github.com/RobinLinus/snapdrop/issues/560) ### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
* Files are transferred after a request is accepted. Files are auto-downloaded upon completing a transfer, if possible. * Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
* Multiple files are downloaded as a ZIP file * Multiple files are downloaded as a ZIP file
* Download, share or save to gallery via the "Share" menu on Android and iOS. * On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
* Multiple files are transferred at once with an overall progress indicator. * Multiple files are transferred at once with an overall progress indicator
### Send Files or Text Directly From Share Menu, Context Menu or CLI ### Send Files or Text Directly From Share Menu, Context Menu or CLI
* [Send files directly from context menu on Ubuntu (using Nautilus)](docs/how-to.md#send-multiple-files-and-directories-directly-from-context-menu-on-ubuntu-using-nautilus) * [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send files directly from the context menu on Windows](docs/how-to.md#send-files-directly-from-context-menu-on-windows) * [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from the "Share" menu on iOS](docs/how-to.md#send-directly-from-share-menu-on-ios) * [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
* [Send directly from the "Share" menu on Android](docs/how-to.md#send-directly-from-share-menu-on-android) * [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
* [Send directly via the command-line interface](docs/how-to.md#send-directly-via-command-line-interface)
### Other Changes ### Other changes
* Change your display name to easily differentiate your devices. * Change your display name permanently to easily differentiate your devices
* [Paste files/text and choose the recipient afterwards ](https://github.com/RobinLinus/snapdrop/pull/534) * [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413) * [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
* Warn user before PairDrop is closed on file transfer * Warn user before PairDrop is closed on file transfer
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101)) * Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
* [Video and audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads)) * [Video and Audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads))
* Switch theme back to auto/system after dark or light mode is on * Switch theme back to auto/system after darkmode or lightmode is enabled
* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario)) * Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))
* Auto-restart on error (Thanks [@KaKi87](https://github.com/KaKi87)) * Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself, you can [set your own STUN/TURN servers](docs/host-your-own.md#specify-stunturn-servers) * When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
* Translations. * Built-in translations
</details> </details>
## Translate PairDrop on [Hosted Weblate](https://hosted.weblate.org/engage/pairdrop/) ## Screenshots
<a href="https://hosted.weblate.org/engage/pairdrop/"> <img src="https://raw.githubusercontent.com/schlagmichdoch/PairDrop/master/docs/pairdrop_screenshot_mobile.gif" style="max-height: 50vh">
<img src="https://hosted.weblate.org/widget/pairdrop/horizontal-blue.svg" alt="Translation status" style="width: 300px" />
</a>
## Built with the following awesome technologies: ## PairDrop is built with the following awesome technologies:
* Vanilla HTML5 / JS ES6 / CSS 3 frontend * Vanilla HTML5 / ES6 / CSS3 frontend
* [WebRTC](http://webrtc.org/) / WebSockets * [WebRTC](http://webrtc.org/) / [WebSockets](http://www.websocket.org/)
* [Node.js](https://nodejs.org/en/) backend * [NodeJS](https://nodejs.org/en/) backend
* [Progressive web app (PWA)](https://en.wikipedia.org/wiki/Progressive_web_app) unified functionality * [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) storage handling * [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
* [zip.js](https://gildas-lormeau.github.io/zip.js/) library * [zip.js](https://gildas-lormeau.github.io/zip.js/)
* [cyrb53](https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js) super-fast hash function * [cyrb53](https://github.com/bryc) super fast hash function
* [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep)) * [Weblate](https://weblate.org/) Web based localization tool
* [libheif](https://github.com/strukturag/libheif) library to handle HEIC/HEIF files (GPLv3)
* [Weblate](https://weblate.org/) web-based localization tool
[FAQ](docs/faq.md) Have any questions? Read our [FAQ](/docs/faq.md).
[Host your own instance with Docker or Node.js](docs/host-your-own.md). You can [host your own instance with Docker](/docs/host-your-own.md).
## Support
## Support PairDrop
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank"> <a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy me a coffee" style="height: 60px !important;width: 217px !important;" > <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
</a> </a>
<br />
<br />
PairDrop is libre, and always will be. \ PairDrop is free and always will be.
If you find it useful and want to support free and open-source software, please consider donating using the button above. \ Still, we have to pay for the domain and the server.
I footed the bill for the domain and the server, and you can help create and maintain great software by supporting me. \
Thank you very much for your contribution! To contribute and support, please use BuyMeACoffee via the button above.
Thanks a lot for supporting free and open software!
## Translate PairDrop
<a href="https://hosted.weblate.org/engage/pairdrop/">
<img src="https://hosted.weblate.org/widget/pairdrop/pairdrop-spa/open-graph.png" alt="Translation status" style="max-height: 30vh" />
</a>
## How to contribute
## Contributing
Feel free to [open an issue](https://github.com/schlagmichdoch/pairdrop/issues/new/choose) or a Feel free to [open an issue](https://github.com/schlagmichdoch/pairdrop/issues/new/choose) or a
[pull request](https://github.com/schlagmichdoch/pairdrop/pulls), following the [pull request](https://github.com/schlagmichdoch/pairdrop/pulls) but follow
[Contributing Guidelines](CONTRIBUTING.md). [Contributing Guidelines](/CONTRIBUTING.md).
-3
View File
@@ -1,3 +0,0 @@
FROM nginx:alpine
RUN apk add --no-cache openssl
-42
View File
@@ -1,42 +0,0 @@
server {
listen 80;
expires epoch;
location / {
proxy_connect_timeout 300;
proxy_pass http://pairdrop:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
}
location /ca.crt {
alias /etc/ssl/certs/pairdropCA.crt;
}
# To allow POST on static pages
error_page 405 =200 $uri;
}
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;
ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;
expires epoch;
location / {
proxy_connect_timeout 300;
proxy_pass http://pairdrop:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
}
location /ca.crt {
alias /etc/ssl/certs/pairdropCA.crt;
}
# To allow POST on static pages
error_page 405 =200 $uri;
}
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
cnf_dir='/mnt/openssl/'
certs_dir='/etc/ssl/certs/'
openssl req -config ${cnf_dir}pairdropCA.cnf -new -x509 -days 1 -keyout ${certs_dir}pairdropCA.key -out ${certs_dir}pairdropCA.crt
openssl req -config ${cnf_dir}pairdropCert.cnf -new -out /tmp/pairdrop-dev.csr -keyout ${certs_dir}pairdrop-dev.key
openssl x509 -req -in /tmp/pairdrop-dev.csr -CA ${certs_dir}pairdropCA.crt -CAkey ${certs_dir}pairdropCA.key -CAcreateserial -extensions req_ext -extfile ${cnf_dir}pairdropCert.cnf -sha512 -days 1 -out ${certs_dir}pairdrop-dev.crt
exec "$@"
-26
View File
@@ -1,26 +0,0 @@
[ req ]
default_bits = 2048
default_md = sha256
default_days = 1
encrypt_key = no
distinguished_name = subject
x509_extensions = x509_ext
string_mask = utf8only
prompt = no
[ subject ]
organizationName = PairDrop
OU = CA
commonName = pairdrop-CA
[ x509_ext ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
# You only need digitalSignature below. *If* you don't allow
# RSA Key transport (i.e., you use ephemeral cipher suites), then
# omit keyEncipherment because that's key transport.
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, digitalSignature, keyEncipherment, cRLSign, keyCertSign
-29
View File
@@ -1,29 +0,0 @@
[ req ]
default_bits = 2048
default_md = sha256
default_days = 1
encrypt_key = no
distinguished_name = subject
req_extensions = req_ext
string_mask = utf8only
prompt = no
[ subject ]
organizationName = PairDrop
OU = Development
# Use a friendly name here because it's presented to the user. The server's DNS
# names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
# by both IETF and CA/Browser Forums. If you place a DNS name here, then you
# must include the DNS name in the SAN too (otherwise, Chrome and others that
# strictly follow the CA/Browser Baseline Requirements will fail).
commonName = ${ENV::FQDN}
[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = DNS:${ENV::FQDN}
nsComment = "OpenSSL Generated Certificate"
extendedKeyUsage = serverAuth
+11 -23
View File
@@ -1,31 +1,19 @@
version: "3" version: "3"
services: services:
pairdrop: node:
image: "lscr.io/linuxserver/pairdrop:latest" image: "node:lts-alpine"
container_name: pairdrop user: "node"
restart: unless-stopped working_dir: /home/node/app
volumes: volumes:
- ./rtc_config.json:/home/node/app/rtc_config.json - ./:/home/node/app
environment: command: ash -c "npm i && npm run start:prod"
- PUID=1000 # UID to run the application as restart: unless-stopped
- PGID=1000 # GID to run the application as
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
- RTC_CONFIG=/home/node/app/rtc_config.json # Set to the path of a file that specifies the STUN/TURN servers.
- DEBUG_MODE=false # Set to true to debug container and peer connections.
- TZ=Etc/UTC # Time Zone
ports: ports:
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000` - "3000:3000"
coturn_server: coturn_server:
image: "coturn/coturn" image: "coturn/coturn"
restart: unless-stopped restart: always
network_mode: "host"
volumes: volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf - ./turnserver.conf:/etc/coturn/turnserver.conf
- ./ssl/:/etc/coturn/ssl/ #you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password
ports:
- "3478:3478"
- "3478:3478/udp"
- "5349:5349"
- "5349:5349/udp"
- "10000-20000:10000-20000/udp"
# see guide at docs/host-your-own.md#coturn-and-pairdrop-via-docker-compose
-34
View File
@@ -1,34 +0,0 @@
version: "3"
services:
pairdrop:
build: .
container_name: pairdrop
restart: unless-stopped
environment:
- PUID=1000 # UID to run the application as
- PGID=1000 # GID to run the application as
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
- RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.
- DEBUG_MODE=false # Set to true to debug container and peer connections.
- TZ=Etc/UTC # Time Zone
ports:
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`
nginx:
build:
context: dev/
dockerfile: nginx-with-openssl.Dockerfile
image: "nginx-with-openssl"
volumes:
- ./public:/usr/share/nginx/html
- ./dev/certs:/etc/ssl/certs
- ./dev/openssl:/mnt/openssl
- ./dev/nginx/default.conf:/etc/nginx/conf.d/default.conf
ports:
- "8080:80"
- "8443:443"
environment:
- FQDN=localhost
entrypoint: /mnt/openssl/create.sh
command: ["nginx", "-g", "daemon off;"]
restart: unless-stopped
+8 -12
View File
@@ -1,16 +1,12 @@
version: "3" version: "3"
services: services:
pairdrop: node:
image: "lscr.io/linuxserver/pairdrop:latest" image: "node:lts-alpine"
container_name: pairdrop user: "node"
working_dir: /home/node/app
volumes:
- ./:/home/node/app
command: ash -c "npm i && npm run start:prod"
restart: unless-stopped restart: unless-stopped
environment:
- PUID=1000 # UID to run the application as
- PGID=1000 # GID to run the application as
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
- RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.
- DEBUG_MODE=false # Set to true to debug container and peer connections.
- TZ=Etc/UTC # Time Zone
ports: ports:
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000` - "3000:3000"
+16 -74
View File
@@ -5,39 +5,18 @@
Help! I can't install the PWA! Help! I can't install the PWA!
</summary> </summary>
<br> if you are using a Chromium-based browser (Chrome, Edge, Vivaldi, Brave, etc.), you can easily install PairDrop PWA on your desktop
by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).
Here is a good guide on how to install PWAs on different platforms: \
https://www.cdc.gov/niosh/mining/content/hearingloss/installPWA.html
**Chromium-based browser on Desktop (Chrome, Edge, Vivaldi, Brave, etc.)** \
Easily install PairDrop PWA on your desktop by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).
<img width="400" src="pwa-install.png" alt="Example on how to install a pwa with Edge"> <img width="400" src="pwa-install.png" alt="Example on how to install a pwa with Edge">
**Desktop Firefox** \
On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/)
**Android** \
PWAs are installable only by using Google Chrome or Samsung Browser:
1. Visit [pairdrop.net](https://pairdrop.net)
2. Click _Install_ on the installation pop-up or use the three-dot-menu and click on _Add to Home screen_
3. Click _Add_ on the pop-up
**iOS** \
PWAs are installable only by using Safari:
1. Visit [pairdrop.net](https://pairdrop.net)
2. Click on the share icon
3. Click _Add to Home Screen_
4. Click _Add_ in the top right corner
<br> <br>
**Self-Hosted Instance?** \ <b>Self-Hosted Instance?</b>
To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs). To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info. See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
<br> <br>
</details> </details>
@@ -47,9 +26,7 @@ See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob
Shortcuts? Shortcuts?
</summary> </summary>
<br> Shortcuts
Available shortcuts:
- Send a message with `CTRL + ENTER` - Send a message with `CTRL + ENTER`
- Close all "Send" and "Pair" dialogs by pressing `Esc`. - Close all "Send" and "Pair" dialogs by pressing `Esc`.
- Copy a received message to the clipboard with `CTRL/⌘ + C`. - Copy a received message to the clipboard with `CTRL/⌘ + C`.
@@ -63,20 +40,12 @@ Available shortcuts:
How to save images directly to the gallery on iOS? How to save images directly to the gallery on iOS?
</summary> </summary>
<br> Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.
It simply does not offer that option for images shared from a website.
~~Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.~~ iOS Shortcuts saves the day:
~~It simply does not offer that option for images shared from a website.~~
~~iOS Shortcuts saves the day:~~ \
I created a simple iOS shortcut that takes your photos and saves them to your gallery: I created a simple iOS shortcut that takes your photos and saves them to your gallery:
https://routinehub.co/shortcut/13988/ https://routinehub.co/shortcut/13988/
Update: \
Apparently, this was only a bug that is fixed in recent iOS version (https://github.com/WebKit/WebKit/pull/13111). \
If you use an older affected iOS version this might still be of use. \
Luckily, you can now simply use `Save Image`/`Save X Images` 🎉
<br> <br>
</details> </details>
@@ -86,13 +55,10 @@ Luckily, you can now simply use `Save Image`/`Save X Images` 🎉
Is it possible to send files or text directly from the "Context" or "Share" menu? Is it possible to send files or text directly from the "Context" or "Share" menu?
</summary> </summary>
<br>
Yes, it finally is. Yes, it finally is.
* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) * [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) * [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) * [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
<br> <br>
</details> </details>
@@ -102,12 +68,9 @@ Yes, it finally is.
Is it possible to send files or text directly via CLI? Is it possible to send files or text directly via CLI?
</summary> </summary>
<br>
Yes. Yes.
* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) * [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
<br> <br>
</details> </details>
@@ -117,14 +80,11 @@ Yes.
Are there any third-party Apps? Are there any third-party Apps?
</summary> </summary>
<br>
These third-party apps are compatible with PairDrop: These third-party apps are compatible with PairDrop:
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) 1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) 2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
3. Feel free to make one :) 3. Feel free to make one :)
<br> <br>
</details> </details>
@@ -134,8 +94,6 @@ These third-party apps are compatible with PairDrop:
What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server? What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server?
</summary> </summary>
<br>
It uses a WebRTC peer-to-peer connection. It uses a WebRTC peer-to-peer connection.
WebRTC needs a signaling server that is only used to establish a connection. WebRTC needs a signaling server that is only used to establish a connection.
The server is not involved in the file transfer. The server is not involved in the file transfer.
@@ -151,7 +109,6 @@ to learn more about STUN, TURN and WebRTC.
If you host your own instance If you host your own instance
and want to support devices that do not support WebRTC, and want to support devices that do not support WebRTC,
you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn). you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
<br> <br>
</details> </details>
@@ -161,11 +118,9 @@ you can [start the PairDrop instance with an activated WebSocket fallback](https
What about privacy? Will files be saved on third-party servers? What about privacy? Will files be saved on third-party servers?
</summary> </summary>
<br>
Files are sent directly between peers. Files are sent directly between peers.
PairDrop doesn't even use a database. PairDrop doesn't even use a database.
If curious, study [the signaling server](https://github.com/schlagmichdoch/PairDrop/blob/master/server/ws-server.js). If curious, study [the server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
WebRTC encrypts the files in transit. WebRTC encrypts the files in transit.
If the devices are on the same network, If the devices are on the same network,
@@ -175,7 +130,6 @@ If your devices are paired and behind a NAT,
the PairDrop TURN Server is used to route your files and messages. the PairDrop TURN Server is used to route your files and messages.
See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)
to learn more about STUN, TURN and WebRTC. to learn more about STUN, TURN and WebRTC.
<br> <br>
</details> </details>
@@ -185,12 +139,10 @@ to learn more about STUN, TURN and WebRTC.
What about security? Are my files encrypted while sent between the computers? What about security? Are my files encrypted while sent between the computers?
</summary> </summary>
<br>
Yes. Your files are sent using WebRTC, encrypting them in transit. Yes. Your files are sent using WebRTC, encrypting them in transit.
Still you have to trust the PairDrop server. To ensure the connection is secure and there is no [MITM](https://en.m.wikipedia.org/wiki/Man-in-the-middle_attack) there is a plan to make PairDrop To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack),
zero trust by encrypting the signaling and implementing a verification process. See [issue #180](https://github.com/schlagmichdoch/PairDrop/issues/180) to keep updated. compare the security number shown under the device name on both devices.
The security number is different for every connection.
<br> <br>
</details> </details>
@@ -200,8 +152,6 @@ zero trust by encrypting the signaling and implementing a verification process.
Transferring many files with paired devices takes too long Transferring many files with paired devices takes too long
</summary> </summary>
<br>
Naturally, if traffic needs to be routed through the TURN server Naturally, if traffic needs to be routed through the TURN server
because your devices are behind different NATs, transfer speed decreases. because your devices are behind different NATs, transfer speed decreases.
@@ -214,7 +164,6 @@ which omits the need of the TURN server.
You can also use mobile hotspots on phones to do that. You can also use mobile hotspots on phones to do that.
Then, all data should be sent directly between devices and not use your data plan. Then, all data should be sent directly between devices and not use your data plan.
<br> <br>
</details> </details>
@@ -224,8 +173,6 @@ Then, all data should be sent directly between devices and not use your data pla
Why don't you implement feature xyz? Why don't you implement feature xyz?
</summary> </summary>
<br>
Snapdrop and PairDrop are a study in radical simplicity. Snapdrop and PairDrop are a study in radical simplicity.
The user interface is insanely simple. The user interface is insanely simple.
Features are chosen very carefully because complexity grows quadratically Features are chosen very carefully because complexity grows quadratically
@@ -237,24 +184,22 @@ Don't be sad. We may decline your feature request for the sake of simplicity.
Read *Insanely Simple: The Obsession that Drives Apple's Success*, Read *Insanely Simple: The Obsession that Drives Apple's Success*,
and/or *Thinking, Fast and Slow* to learn more. and/or *Thinking, Fast and Slow* to learn more.
<br> <br>
</details> </details>
<details> <details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;"> <summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
PairDrop is awesome. How can I support it? Snapdrop and PairDrop are awesome. How can I support them?
</summary> </summary>
<br>
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software. * [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software.
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues) * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
* Share PairDrop on social media. * Share PairDrop on social media.
* Fix bugs and create a pull request. * Fix bugs and make a pull request.
* Do some security analysis and make suggestions. * Do some security analysis and make suggestions.
* Participate in [active discussions](https://github.com/schlagmichdoch/PairDrop/discussions) * To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop)
<br> <br>
</details> </details>
@@ -264,10 +209,7 @@ and/or *Thinking, Fast and Slow* to learn more.
How does it work? How does it work?
</summary> </summary>
<br>
[See here for info about the technical implementation](/docs/technical-documentation.md) [See here for info about the technical implementation](/docs/technical-documentation.md)
<br> <br>
</details> </details>
+295 -458
View File
@@ -1,132 +1,181 @@
# Deployment Notes # Deployment Notes
The easiest way to get PairDrop up and running is by using Docker.
## TURN server for Internet Transfer > <b>TURN server for Internet Transfer</b>
>
> Beware that you have to host your own TURN server to enable transfers between different networks.
>
> Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \
> or deploy it via docker-compose (Step 5).
Beware that you have to host your own TURN server to enable transfers between different networks. > <b>PairDrop via HTTPS</b>
>
Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) > On some browsers PairDrop must be served over TLS in order for some feautures to work properly. These may include copying an incoming message via the 'copy' button, installing PairDrop as PWA, persistent pairing of devices and changing of the display name, and notifications. Naturally, this is also recommended to increase security.
or deploy it via Docker (Step 5).
You can use the `docker-compose-coturn.yml` in this repository. See [Coturn and PairDrop via Docker Compose](#coturn-and-pairdrop-via-docker-compose).
Alternatively, use a free, pre-configured TURN server like [OpenRelay](https://www.metered.ca/tools/openrelay/)
<br>
## PairDrop via HTTPS
On some browsers PairDrop must be served over TLS in order for some features to work properly.
These may include:
- Copying an incoming message via the 'copy' button
- Installing PairDrop as PWA
- Persistent pairing of devices
- Changing of the display name
- Notifications
Naturally, this is also recommended to increase security.
<br>
## Deployment with Docker ## Deployment with Docker
The easiest way to get PairDrop up and running is by using Docker.
### Docker Image from Docker Hub ### Docker Image from Docker Hub
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
``` ```
> This image is hosted by [linuxserver.io](https://linuxserver.io). For more information visit https://hub.docker.com/r/linuxserver/pairdrop
> You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, \
> `127.0.0.1` is specified in the run command.
<br> #### Options / Flags
Set options by using the following flags in the `docker run` command:
### Docker Image from GitHub Container Registry (ghcr.io)
##### Port
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop -p 127.0.0.1:8080:3000
```
> Specify the port used by the docker image
> - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000`
##### Rate limiting requests
```bash
-e RATE_LIMIT=true
```
> Limits clients to 1000 requests per 5 min
##### IPv6 Localization
```bash
-e IPV6_LOCALIZE=4
```
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments \
> of the client IPv6 address to be evaluated as the peer's IP. \
> This can be especially useful when using Cloudflare as a proxy.
>
> The flag must be set to an **integer** between `1` and `7`. \
> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \
> to match the client IP against. The most common value would be `4`, \
> which will group peers within the same `/64` subnet.
##### Websocket Fallback (for VPN)
```bash
-e WS_FALLBACK=true
```
> Provides PairDrop to clients with an included websocket fallback \
> if the peer to peer WebRTC connection is not available to the client.
>
> This is not used on the official https://pairdrop.net website, \
> but you can activate it on your self-hosted instance.
> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
>
> **Warning:** All traffic sent between devices using this fallback \
> is routed through the server and therefor not peer to peer! \
> Beware that the traffic routed via this fallback is readable by the server. \
> Only ever use this on instances you can trust. \
> Additionally, beware that all traffic using this fallback debits the servers data plan.
##### Specify STUN/TURN Servers
```bash
-e RTC_CONFIG="rtc_config.json"
``` ```
> Specify the STUN/TURN servers PairDrop clients use by setting \
> `RTC_CONFIG` to a JSON file including the configuration. \
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
> Alternatively, use a free, pre-configured TURN server like [OpenRelay]([url](https://www.metered.ca/tools/openrelay/))
>
> Default configuration:
> ```json
> {
> "sdpSemantics": "unified-plan",
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> }
> ]
> }
> ```
##### Debug Mode
```bash
-e DEBUG_MODE="true"
```
> Use this flag to enable debugging information about the connecting peers IP addresses. \
> This is quite useful to check whether the [#HTTP-Server](#http-server) \
> is configured correctly, so the auto-discovery feature works correctly. \
> Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device, everything is set up correctly. \
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
<br> <br>
### Docker Image from GHCR
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
```
> You must use a server proxy to set the X-Forwarded-For to prevent \
> all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the Docker container directly, \
> `127.0.0.1` is specified in the run command.
>
> To specify options replace `npm run start:prod` \
> according to [the documentation below.](#options--flags-1)
> The Docker Image includes a healthcheck. \
> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
### Docker Image self-built ### Docker Image self-built
#### Build the image #### Build the image
```bash ```bash
docker build --pull . -f Dockerfile -t pairdrop docker build --pull . -f Dockerfile -t pairdrop
``` ```
> A GitHub action is set up to do this step automatically.
> A GitHub action is set up to do this step automatically at the release of new versions.
> >
> `--pull` ensures always the latest node image is used. > `--pull` ensures always the latest node image is used.
#### Run the image #### Run the image
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
``` ```
> You must use a server proxy to set the X-Forwarded-For \
> You must use a server proxy to set the `X-Forwarded-For` header
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> >
> To prevent bypassing the proxy by reaching the docker container directly, > To prevent bypassing the proxy by reaching the Docker container \
> `127.0.0.1` is specified in the run command. > directly, `127.0.0.1` is specified in the run command.
<br>
### Flags
Set options by using the following flags in the `docker run` command:
#### Port
```bash
-p 127.0.0.1:8080:3000
```
> Specify the port used by the docker image
> >
> - 3000 -> `-p 127.0.0.1:3000:3000` > To specify options replace `npm run start:prod` \
> - 8080 -> `-p 127.0.0.1:8080:3000` > according to [the documentation below.](#options--flags-1)
#### Set Environment Variables via Docker > The Docker Image includes a Healthcheck. \
Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
Environment Variables are set directly in the `docker run` command: \
e.g. `docker run -p 127.0.0.1:3000:3000 -it pairdrop -e DEBUG_MODE="true"`
Overview of available Environment Variables are found [here](#environment-variables).
Example:
```bash
docker run -d \
--name=pairdrop \
--restart=unless-stopped \
-p 127.0.0.1:3000:3000 \
-e PUID=1000 \
-e PGID=1000 \
-e WS_SERVER=false \
-e WS_FALLBACK=false \
-e RTC_CONFIG=false \
-e RATE_LIMIT=false \
-e DEBUG_MODE=false \
-e TZ=Etc/UTC \
lscr.io/linuxserver/pairdrop
```
<br> <br>
## Deployment with Docker Compose ## Deployment with Docker Compose
Here's an example docker-compose file:
Here's an example docker compose file:
```yaml ```yaml
version: "3" version: "2"
services: services:
pairdrop: pairdrop:
image: "lscr.io/linuxserver/pairdrop:latest" image: lscr.io/linuxserver/pairdrop:latest
container_name: pairdrop container_name: pairdrop
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -134,26 +183,22 @@ services:
- PGID=1000 # GID to run the application as - PGID=1000 # GID to run the application as
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client. - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min. - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
- RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.
- DEBUG_MODE=false # Set to true to debug container and peer connections.
- TZ=Etc/UTC # Time Zone - TZ=Etc/UTC # Time Zone
ports: ports:
- "127.0.0.1:3000:3000" # Web UI - 127.0.0.1:3000:3000 # Web UI
``` ```
Run the compose file with `docker compose up -d`. Run the compose file with `docker compose up -d`.
> You must use a server proxy to set the `X-Forwarded-For` header > You must use a server proxy to set the X-Forwarded-For \
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> >
> To prevent bypassing the proxy by reaching the Docker container > To prevent bypassing the proxy by reaching the Docker container \
> directly, `127.0.0.1` is specified in the `ports` argument. > directly, `127.0.0.1` is specified in the run command.
<br> <br>
## Deployment with Node.js ## Deployment with node
Clone this repository and enter the folder
```bash ```bash
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop
@@ -167,223 +212,56 @@ npm install
Start the server with: Start the server with:
```bash
node index.js
```
or
```bash ```bash
npm start npm start
``` ```
> Remember to check your IP address using your OS command to see where you can access the server.
> By default, the node server listens on port 3000. > By default, the node server listens on port 3000.
<br> <br>
### Options / Flags ### Environment variables
These are some flags only reasonable when deploying via Node.js
#### Port #### Port
```bash
PORT=3000
```
> Default: `3000`
>
> Environment variable to specify the port used by the Node.js server \
> e.g. `PORT=3010 npm start`
#### Local Run
```bash
npm start -- --localhost-only
```
> Only allow connections from localhost.
>
> You must use a server proxy to set the `X-Forwarded-For` header
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> Use this when deploying PairDrop with node to prevent
> bypassing the reverse proxy by reaching the Node.js server directly.
#### Automatic restart on error
```bash
npm start -- --auto-restart
```
> Restarts server automatically on error
#### Production (autostart and rate-limit)
```bash
npm run start:prod
```
> shortcut for `RATE_LIMIT=5 npm start -- --auto-restart`
#### Production (autostart, rate-limit, localhost-only)
```bash
npm run start:prod -- --localhost-only
```
> To prevent connections to the node server from bypassing \
> the proxy server you should always use "--localhost-only" on production.
#### Set Environment Variables via Node.js
To specify environment variables set them in the run command in front of `npm start`.
The syntax is different on Unix and Windows.
On Unix based systems On Unix based systems
```bash ```bash
PORT=3000 RTC_CONFIG="rtc_config.json" npm start PORT=3010 npm start
``` ```
On Windows On Windows
```bash ```bash
$env:PORT=3000 RTC_CONFIG="rtc_config.json"; npm start $env:PORT=3010; npm start
``` ```
> Specify the port PairDrop is running on. (Default: 3000)
Overview of available Environment Variables are found [here](#environment-variables). #### IPv6 Localization
<br>
## Environment Variables
### Debug Mode
```bash
DEBUG_MODE="true"
```
> Default: `false`
>
> Logs the used environment variables for debugging.
>
> Prints debugging information about the connecting peers IP addresses.
>
> This is quite useful to check whether the [#HTTP-Server](#http-server)
> is configured correctly, so the auto-discovery feature works correctly.
> Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
>
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
>
> If the IP address "PairDrop uses" matches the public IP address of the client device, everything is set up correctly. \
> To find out the public IP address of the client device visit https://whatsmyip.com/.
>
> To preserve your clients' privacy: \
> **Never use this environment variable in production!**
<br>
### Rate limiting requests
```bash
RATE_LIMIT=1
```
> Default: `false`
>
> Limits clients to 1000 requests per 5 min
>
> "If you are behind a proxy/load balancer (usually the case with most hosting services, e.g. Heroku, Bluemix, AWS ELB,
> Render, Nginx, Cloudflare, Akamai, Fastly, Firebase Hosting, Rackspace LB, Riverbed Stingray, etc.), the IP address of
> the request might be the IP of the load balancer/reverse proxy (making the rate limiter effectively a global one and
> blocking all requests once the limit is reached) or undefined."
> (See: https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues)
>
> To find the correct number to use for this setting:
>
> 1. Start PairDrop with `DEBUG_MODE=True` and `RATE_LIMIT=1`
> 2. Make a `get` request to `/ip` of the PairDrop instance (e.g. `https://pairdrop-example.net/ip`)
> 3. Check if the IP address returned in the response matches your public IP address (find out by visiting e.g. https://whatsmyip.com/)
> 4. You have found the correct number if the IP addresses match. If not, then increase `RATE_LIMIT` by one and redo 1. - 4.
>
> e.g. on Render you must use RATE_LIMIT=5
<br>
### IPv6 Localization
```bash ```bash
IPV6_LOCALIZE=4 IPV6_LOCALIZE=4
``` ```
> Truncate a portion of the client IPv6 address to make peers more discoverable. \
> See [Options/Flags](#options--flags) above.
> Default: `false` #### Specify STUN/TURN Server
> On Unix based systems
> To enable Peer Auto-Discovery among IPv6 peers, you can specify a reduced number of segments \
> of the client IPv6 address to be evaluated as the peer's IP. \
> This can be especially useful when using Cloudflare as a proxy.
>
> The flag must be set to an **integer** between `1` and `7`. \
> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \
> to match the client IP against. The most common value would be `4`, \
> which will group peers within the same `/64` subnet.
<br>
### Websocket Fallback (for VPN)
```bash ```bash
WS_FALLBACK=true RTC_CONFIG="rtc_config.json" npm start
``` ```
On Windows
> Default: `false`
>
> Provides PairDrop to clients with an included websocket fallback \
> if the peer to peer WebRTC connection is not available to the client.
>
> This is not used on the official https://pairdrop.net website,
> but you can activate it on your self-hosted instance.\
> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in
> order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
>
> **Warning:** \
> All traffic sent between devices using this fallback
> is routed through the server and therefor not peer to peer!
>
> Beware that the traffic routed via this fallback is readable by the server. \
> Only ever use this on instances you can trust.
>
> Additionally, beware that all traffic using this fallback debits the servers data plan.
<br>
### Specify STUN/TURN Servers
```bash ```bash
RTC_CONFIG="rtc_config.json" $env:RTC_CONFIG="rtc_config.json"; npm start
``` ```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` \
> Default: `false` > to a JSON file including the configuration. \
> You can use `pairdrop/rtc_config_example.json` as a starting point.
> >
> Specify the STUN/TURN servers PairDrop clients use by setting \ > To host your own TURN server you can follow this guide: \
> `RTC_CONFIG` to a JSON file including the configuration. \ > https://gabrieltanner.org/blog/turn-server/
> You can use `rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
> Alternatively, use a free, pre-configured TURN server like [OpenRelay](<[url](https://www.metered.ca/tools/openrelay/)>)
> >
> Default configuration: > Default configuration:
>
> ```json > ```json
> { > {
> "sdpSemantics": "unified-plan", > "sdpSemantics": "unified-plan",
@@ -395,90 +273,109 @@ RTC_CONFIG="rtc_config.json"
> } > }
> ``` > ```
<br> #### Debug Mode
On Unix based systems
You can host an instance that uses another signaling server
This can be useful if you don't want to trust the client files that are hosted on another instance but still want to connect to devices that use https://pairdrop.net.
### Specify Signaling Server
```bash ```bash
SIGNALING_SERVER="pairdrop.net" DEBUG_MODE="true" npm start
```
On Windows
```bash
$env:DEBUG_MODE="true"; npm start
``` ```
> Default: `false` > Use this flag to enable debugging info about the connecting peers IP addresses. \
> This is quite useful to check whether the [#HTTP-Server](#http-server) \
> is configured correctly, so the auto discovery feature works correctly. \
> Otherwise, all clients discover each other mutually, independently of their network status.
> >
> By default, clients connecting to your instance use the signaling server of your instance to connect to other devices. > If this flag is set to `"true"` each peer that connects to the \
> PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is set up correctly. \
>Find your devices public IP by visiting https://www.whatismyip.com/.
> >
> By using `SIGNALING_SERVER`, you can host an instance that uses another signaling server. > Preserve your clients' privacy. **Never use this flag in production!**
### Options / Flags
#### Local Run
```bash
npm start -- --localhost-only
```
> Only allow connections from localhost.
> >
> This can be useful if you want to ensure the integrity of the client files and don't want to trust the client files that are hosted on another PairDrop instance but still want to connect to devices that use the other instance. > You must use a server proxy to set the X-Forwarded-For \
> E.g. host your own client files under *pairdrop.your-domain.com* but use the official signaling server under *pairdrop.net* > to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
> This way devices connecting to *pairdrop.your-domain.com* and *pairdrop.net* can discover each other.
> >
> Beware that the version of your PairDrop server must be compatible with the version of the signaling server. > Use this when deploying PairDrop with node to prevent \
> > bypassing the proxy by reaching the Docker container directly.
> `SIGNALING_SERVER` must be a valid url without the protocol prefix.
> Examples of valid values: `pairdrop.net`, `pairdrop.your-domain.com:3000`, `your-domain.com/pairdrop` #### Automatic restart on error
```bash
npm start -- --auto-restart
```
> Restarts server automatically on error
<br> <br>
### Customizable buttons for the _About PairDrop_ page #### Rate limiting requests
```bash ```bash
DONATION_BUTTON_ACTIVE=true npm start -- --rate-limit
DONATION_BUTTON_LINK="https://www.buymeacoffee.com/pairdrop" ```
DONATION_BUTTON_TITLE="Buy me a coffee" > Limits clients to 1000 requests per 5 min
TWITTER_BUTTON_ACTIVE=true
TWITTER_BUTTON_LINK="https://twitter.com/account" <br>
TWITTER_BUTTON_TITLE="Find me on Twitter"
MASTODON_BUTTON_ACTIVE=true #### Websocket Fallback (for VPN)
MASTODON_BUTTON_LINK="https://mastodon.social/account" ```bash
MASTODON_BUTTON_TITLE="Find me on Mastodon" npm start -- --include-ws-fallback
BLUESKY_BUTTON_ACTIVE=true ```
BLUESKY_BUTTON_LINK="https://bsky.app/profile/account" > Provides PairDrop to clients with an included websocket fallback \
BLUESKY_BUTTON_TITLE="Find me on Bluesky" > if the peer to peer WebRTC connection is not available to the client.
CUSTOM_BUTTON_ACTIVE=true >
CUSTOM_BUTTON_LINK="https://your-custom-social-network.net/account" > This is not used on the official https://pairdrop.net, \
CUSTOM_BUTTON_TITLE="Find me on this custom social network" but you can activate it on your self-hosted instance. \
PRIVACYPOLICY_BUTTON_ACTIVE=true > This is especially useful if you connect to your instance \
PRIVACYPOLICY_BUTTON_LINK="https://link-to-your-privacy-policy.net" > via a VPN as most VPN services block WebRTC completely in order to hide your real IP address.
PRIVACYPOLICY_BUTTON_TITLE="Open our privacy policy" > ([Read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
>
> **Warning:** All traffic sent between devices using this fallback \
> is routed through the server and therefor not peer to peer! \
> Beware that the traffic routed via this fallback is readable by the server. \
> Only ever use this on instances you can trust. \
> Additionally, beware that all traffic using this fallback debits the servers data plan.
<br>
#### Production (autostart and rate-limit)
```bash
npm run start:prod
``` ```
> Default: unset #### Production (autostart, rate-limit, localhost-only and websocket fallback for VPN)
> ```bash
> By default, clients will show the default button configuration: GitHub, BuyMeACoffee, Twitter, and FAQ on GitHub. npm run start:prod -- --localhost-only --include-ws-fallback
> ```
> The GitHub and FAQ on GitHub buttons are essential, so they are always shown. > To prevent connections to the node server from bypassing \
> > the proxy server you should always use "--localhost-only" on production.
> The other buttons can be customized:
>
> * `*_BUTTON_ACTIVE`: set this to `true` to show a natively hidden button or to `false` to hide a normally shown button
> * `*_BUTTON_LINK`: set this to any URL to overwrite the href attribute of the button
> * `*_BUTTON_TITLE`: set this to overwrite the hover title of the button. This will prevent the title from being translated.
<br>
## Healthcheck
> The Docker Image hosted on `ghcr.io` and the self-built Docker Image include a healthcheck.
>
> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
<br>
## HTTP-Server ## HTTP-Server
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \ When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \
Otherwise, all clients will be mutually visible. Otherwise, all clients will be mutually visible.
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode). To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
### Using nginx ### Using nginx
#### Allow http and https requests #### Allow http and https requests
``` ```
server { server {
listen 80; listen 80;
@@ -512,7 +409,6 @@ server {
``` ```
#### Automatic http to https redirect: #### Automatic http to https redirect:
``` ```
server { server {
listen 80; listen 80;
@@ -541,169 +437,110 @@ server {
} }
``` ```
<br>
### Using Apache ### Using Apache
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel` install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
```bash ```bash
a2enmod proxy a2enmod proxy
``` ```
```bash ```bash
a2enmod proxy_http a2enmod proxy_http
``` ```
```bash
a2enmod proxy_wstunnel
```
<br> <br>
Create a new configuration file under `/etc/apache2/sites-available` (on Debian) Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
**pairdrop.conf** **pairdrop.conf**
#### Allow HTTP and HTTPS requests #### Allow HTTP and HTTPS requests
```apacheconf ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket ProxyPass / http://127.0.0.1:3000/
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:3000/$1" [P,L]
</VirtualHost> </VirtualHost>
<VirtualHost *:443> <VirtualHost *:443>
ProxyPass / https://127.0.0.1:3000/ upgrade=websocket ProxyPass / https://127.0.0.1:3000/
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
</VirtualHost> </VirtualHost>
``` ```
#### Automatic HTTP to HTTPS redirect: #### Automatic HTTP to HTTPS redirect:
```apacheconf ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
Redirect permanent / https://127.0.0.1:3000/ Redirect permanent / https://127.0.0.1:3000/
</VirtualHost> </VirtualHost>
<VirtualHost *:443> <VirtualHost *:443>
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket ProxyPass / https://127.0.0.1:3000/
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
</VirtualHost> </VirtualHost>
``` ```
Activate the new virtual host and reload Apache: Activate the new virtual host and reload Apache:
```bash ```bash
a2ensite pairdrop a2ensite pairdrop
``` ```
```bash ```bash
service apache2 reload service apache2 reload
``` ```
<br> # Local Development
## Install
All files needed for developing are available on the branch `dev`.
## Coturn and PairDrop via Docker Compose First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
### Setup container
To run coturn and PairDrop at once by using the `docker-compose-coturn.yml` with TURN over TLS enabled
you need to follow these steps:
1. Generate or retrieve certificates for your `<DOMAIN>` (e.g. letsencrypt / certbot)
2. Create `./ssl` folder: `mkdir ssl`
3. Copy your ssl-certificates and the privkey to `./ssl`
4. Restrict access to `./ssl`: `chown -R nobody:nogroup ./ssl`
5. Create a dh-params file: `openssl dhparam -out ./ssl/dhparams.pem 4096`
6. Copy `rtc_config_example.json` to `rtc_config.json`
7. Copy `turnserver_example.conf` to `turnserver.conf`
8. Change `<DOMAIN>` in both files to the domain where your PairDrop instance is running
9. Change `username` and `password` in `turnserver.conf` and `rtc-config.json`
10. To start the container including coturn run: \
`docker compose -f docker-compose-coturn.yml up -d`
<br>
#### Setup container
To restart the container including coturn run: \
`docker compose -f docker-compose-coturn.yml restart`
<br>
#### Setup container
To stop the container including coturn run: \
`docker compose -f docker-compose-coturn.yml stop`
<br>
### Firewall
To run PairDrop including its own coturn-server you need to punch holes in the firewall. These ports must be opened additionally:
- 3478 tcp/udp
- 5349 tcp/udp
- 10000:20000 tcp/udp
<br>
## Local Development
### Install
All files needed for developing are available in the folder `./dev`.
For convenience, there is also a docker compose file for developing:
#### Developing with docker compose
First, [Install docker with docker compose.](https://docs.docker.com/compose/install/)
Then, clone the repository and run docker compose:
Then, clone the repository and run docker-compose:
```bash ```bash
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop git clone https://github.com/schlagmichdoch/PairDrop.git
```
```bash
docker compose -f docker-compose-dev.yml up --no-deps --build
```
cd PairDrop
git checkout dev
docker-compose up -d
```
Now point your web browser to `http://localhost:8080`. Now point your web browser to `http://localhost:8080`.
- To debug the Node.js server, run `docker logs pairdrop`. - To restart the containers, run `docker-compose restart`.
- After changes to the code you have to rerun the `docker compose` command - To stop the containers, run `docker-compose stop`.
- To debug the NodeJS server, run `docker logs pairdrop_node_1`.
<br> <br>
#### Testing PWA related features ## Testing PWA related features
PWAs requires the app to be served under a correctly set up and trusted TLS endpoint. PWAs requires the app to be served under a correctly set up and trusted TLS endpoint.
The NGINX container creates a CA certificate and a website certificate for you. The NGINX container creates a CA certificate and a website certificate for you. \
To correctly set the common name of the certificate, To correctly set the common name of the certificate, \
you need to change the FQDN environment variable in `docker-compose-dev.yml` you need to change the FQDN environment variable in `docker/fqdn.env` \
to the fully qualified domain name of your workstation. (Default: localhost) to the fully qualified domain name of your workstation.
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \ If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \
For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \ For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \
Install that certificate to the trust store of your operating system. \ Install that certificate to the trust store of your operating system. \
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. \
##### Windows - On macOS, double-click the installed CA certificate in `Keychain Access`, \
- Make sure to install it to the `Trusted Root Certification Authorities` store. - expand `Trust`, and select `Always Trust` for SSL. \
- Firefox uses its own trust store. To install the CA, \
##### macOS - point Firefox at `http://<Your FQDN>:8080/ca.crt`. \
- Double-click the installed CA certificate in `Keychain Access`, - When prompted, select `Trust this CA to identify websites` and click *OK*. \
- expand `Trust`, and select `Always Trust` for SSL. - When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \
- Additionally, after installing a new cert, \
##### Firefox - you need to clear the Storage (DevTools → Application → Clear storage → Clear site data).
Firefox uses its own trust store. To install the CA:
- point Firefox at `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
- When prompted, select `Trust this CA to identify websites` and click _OK_.
Alternatively:
1. Download `ca.crt` from `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
2. Go to `about:preferences#privacy` scroll down to `Security` and `Certificates` and click `View Certificates`
3. Import the downloaded certificate file (step 1)
##### Chrome
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`).
- Additionally, after installing a new cert, you need to clear the Storage (DevTools → Application → Clear storage → Clear site data).
##### Google Chrome
- To skip the installation of the certificate, you can also open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
- The feature `Insecure origins treated as secure` must be enabled and the list must include your PairDrop test instance. E.g.: `http://127.0.0.1:3000,https://127.0.0.1:8443`
Please note that the certificates (CA and webserver cert) expire after a day. Please note that the certificates (CA and webserver cert) expire after a day.
Also, whenever you restart the NGINX Docker container new certificates are created. Also, whenever you restart the NGINX Docker, container new certificates are created.
The site is served on `https://<Your FQDN>:8443` (Default: `https://localhost:8443`). The site is served on `https://<Your FQDN>:8443`.
[< Back](/README.md) [< Back](/README.md)
+52 -110
View File
@@ -1,142 +1,84 @@
# How-To # How-To
## Send files directly from context menu on Windows
### Registering to open files with PairDrop
The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files) is implemented
This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows.
1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api)
2. Install PairDrop by visiting https://pairdrop.net/ with the Edge web browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-).
3. You are done! You can now send most files one at a time via PairDrop:
_context menu > Open with > PairDrop_
[//]: # (Todo: add screenshots)
### Sending multiple files to PairDrop
Outstandingly, it is also possible to send multiple files to PairDrop \
via the context menu by adding PairDrop to the `Send to` menu:
1. [Register PairDrop as file handler](#registering-to-open-files-with-pairdrop)
2. Hit Windows Key+R, type: `shell:programs` and hit Enter.
3. Copy the PairDrop shortcut from the directory
4. Hit Windows Key+R, type: `shell:sendto` and hit Enter.
5. Paste the copied shortcut into the directory
6. You are done! You can now send multiple files (but no directories) directly via PairDrop:
_context menu > Send to > PairDrop_
[//]: # (Todo: add screenshots)
## Send directly from share menu on iOS ## Send directly from share menu on iOS
I created an iOS shortcut to send images, files, folder, URLs \ I created an iOS shortcut to send images, files, folder, URLs \
or text directly from the share-menu or text directly from the share-menu
https://routinehub.co/shortcut/13990/ https://routinehub.co/shortcut/13990/
[//]: # (Todo: Add screenshots) [//]: # (Todo: add doku with screenshots)
<br>
## Send directly from share menu on Android ## Send directly from share menu on Android
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented. The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented.
When the PWA is installed, it will register itself to the share-menu of the device automatically. When the PWA is installed, it will register itself to the share-menu of the device automatically.
<br>
## Send directly via command-line interface ## Send directly via command-line interface
Send files or text with PairDrop via command-line interface. \ Send files or text with PairDrop via command-line interface.
This opens PairDrop in the default browser where you can choose the receiver. This opens PairDrop in the default browser where you can choose the receiver.
### Usage ### Usage
```bash ```bash
pairdrop -h $ pairdrop -h
``` Current domain: https://pairdrop.net/
```
Send files or text with PairDrop via command-line interface.
Current domain: https://pairdrop-dev.onrender.com/
Usage: Usage:
Open PairDrop: pairdrop Open PairDrop: pairdrop
Send files: pairdrop file1/directory1 (file2/directory2 file3/directory3 ...) Send files: pairdrop file/directory
Send text: pairdrop -t "text" Send text: pairdrop -t "text"
Specify domain: pairdrop -d "https://pairdrop.net/" Specify domain: pairdrop -d "https://pairdrop.net/"
Show this help text: pairdrop (-h|--help) Show this help text: pairdrop (-h|--help)
This pairdrop-cli version was released alongside v1.10.4
``` ```
<br> On Windows Command Prompt you need to use bash: `bash pairdrop -h`
### Setup ### Setup
Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
#### Linux / Mac #### Linux
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases) 1. Put the file in a preferred folder e.g. `/usr/local/bin`
```shell 2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop`
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.10/pairdrop-cli.zip" 3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing
``` `export PATH=$PATH:/opt/pairdrop-cli`
or
```shell
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.10/pairdrop-cli.zip"
```
2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`
```shell
sudo unzip pairdrop-cli.zip -d /usr/share/pairdrop-cli/
```
3. Copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_
```shell
sudo cp /usr/share/pairdrop-cli/.pairdrop-cli-config.example /usr/share/pairdrop-cli/.pairdrop-cli-config
```
4. Make the bash file _pairdrop_ executable
```shell
sudo chmod +x /usr/share/pairdrop-cli/pairdrop
```
5. Add a symlink to /usr/local/bin/ to include _pairdrop_ to _PATH_
```shell
sudo ln -s /usr/share/pairdrop-cli/pairdrop /usr/local/bin/pairdrop
```
<br> #### Mac
1. add bash file to `/usr/local/bin`
#### Windows #### Windows
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases) 1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli`
2. Put file in a preferred folder e.g. `C:\Program Files\pairdrop-cli` 2. Search for and open `Edit environment variables for your account`
3. Inside this folder, copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_ 3. Click `Environment Variables…`
4. Search for and open `Edit environment variables for your account` 4. Under *System Variables* select `Path` and click *Edit...*
5. Click `Environment Variables…` 5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed
6. Under _System Variables_ select `Path` and click _Edit..._ 6. Reopen Command prompt window
7. Click _New_, insert the preferred folder (`C:\Program Files\pairdrop-cli`), click *OK* until all windows are closed
8. Reopen Command prompt window
**Requirements**
As Windows cannot execute bash scripts natively, you need to install [Git Bash](https://gitforwindows.org/).
Then, you can also use pairdrop-cli from the default Windows Command Prompt
by using the shell file instead of the bash file which then itself executes
_pairdrop-cli_ (the bash file) via the Git Bash.
```shell
pairdrop.sh -h
```
<br>
## Send multiple files and directories directly from context menu on Windows
### Registering to open files with PairDrop
It is possible to send multiple files with PairDrop via the context menu by adding pairdrop-cli to Windows `Send to` menu:
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)
2. Unzip the archive to a folder of your choice e.g. `C:\Program Files\pairdrop-cli\`
3. Inside this folder, copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_
4. Copy the shortcut _send with PairDrop.lnk_
5. Hit Windows Key+R, type: `shell:sendto` and hit Enter.
6. Paste the copied shortcut into the directory
7. Open the properties window of the shortcut and edit the link field to point to _send-with-pairdrop.ps1_ located in the folder you used in step 2: \
`"C:\Program Files\PowerShell\7\pwsh.exe" -File "C:\Program Files\pairdrop-cli\send-with-pairdrop.ps1"`
8. You are done! You can now send multiple files and directories directly via PairDrop:
_context menu_ > _Send to_ > _PairDrop_
##### Requirements
As Windows cannot execute bash scripts natively, you need to install [Git Bash](https://gitforwindows.org/).
<br>
## Send multiple files and directories directly from context menu on Ubuntu using Nautilus
### Registering to open files with PairDrop
It is possible to send multiple files with PairDrop via the context menu by adding pairdrop-cli to Nautilus `Scripts` menu:
1. Register _pairdrop_ as executable via [guide above](#linux).
2. Copy the shell file _send-with-pairdrop_ to `~/.local/share/nautilus/scripts/` to include it in the context menu
```shell
cp /usr/share/pairdrop-cli/send-with-pairdrop ~/.local/share/nautilus/scripts/
```
3. Make the shell file _send-with-pairdrop_ executable
```shell
chmod +x ~/.local/share/nautilus/scripts/send-with-pairdrop
```
4. You are done! You can now send multiple files and directories directly via PairDrop:
_context menu_ > _Scripts_ > _send-with-pairdrop_
<br>
## File Handling API
The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files)
was implemented, but it was removed as default file associations were overwritten ([#17](https://github.com/schlagmichdoch/PairDrop/issues/17),
[#116](https://github.com/schlagmichdoch/PairDrop/issues/116) [#190](https://github.com/schlagmichdoch/PairDrop/issues/190))
and it only worked with explicitly specified file types and couldn't handle directories at all.
[< Back](/README.md) [< Back](/README.md)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 326 KiB

+852
View File
@@ -0,0 +1,852 @@
const process = require('process')
const crypto = require('crypto')
const {spawn} = require('child_process')
const WebSocket = require('ws');
const fs = require('fs');
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
const express = require('express');
const RateLimit = require('express-rate-limit');
const http = require('http');
// Handle SIGINT
process.on('SIGINT', () => {
console.info("SIGINT Received, exiting...")
process.exit(0)
})
// Handle SIGTERM
process.on('SIGTERM', () => {
console.info("SIGTERM Received, exiting...")
process.exit(0)
})
// Handle APP ERRORS
process.on('uncaughtException', (error, origin) => {
console.log('----- Uncaught exception -----')
console.log(error)
console.log('----- Exception origin -----')
console.log(origin)
})
process.on('unhandledRejection', (reason, promise) => {
console.log('----- Unhandled Rejection at -----')
console.log(promise)
console.log('----- Reason -----')
console.log(reason)
})
if (process.argv.includes('--auto-restart')) {
process.on(
'uncaughtException',
() => {
process.once(
'exit',
() => spawn(
process.argv.shift(),
process.argv,
{
cwd: process.cwd(),
detached: true,
stdio: 'inherit'
}
)
);
process.exit();
}
);
}
const rtcConfig = process.env.RTC_CONFIG
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
: {
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
}
]
};
const app = express();
if (process.argv.includes('--rate-limit')) {
const limiter = RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 1000, // Limit each IP to 1000 requests per `window` (here, per 5 minutes)
message: 'Too many requests from this IP Address, please try again after 5 minutes.',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
})
app.use(limiter);
// ensure correct client ip and not the ip of the reverse proxy is used for rate limiting on render.com
// see https://github.com/express-rate-limit/express-rate-limit#troubleshooting-proxy-issues
app.set('trust proxy', 5);
}
if (process.argv.includes('--include-ws-fallback')) {
app.use(express.static('public_included_ws_fallback'));
} else {
app.use(express.static('public'));
}
const debugMode = process.env.DEBUG_MODE === "true";
if (debugMode) {
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
}
let ipv6_lcl;
if (process.env.IPV6_LOCALIZE) {
ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE);
if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) {
console.error("IPV6_LOCALIZE must be an integer between 1 and 7");
return;
}
console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments");
}
app.use(function(req, res) {
res.redirect('/');
});
app.get('/', (req, res) => {
res.sendFile('index.html');
});
const server = http.createServer(app);
const port = process.env.PORT || 3000;
if (process.argv.includes('--localhost-only')) {
server.listen(port, '127.0.0.1');
} else {
server.listen(port);
}
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(err);
console.info("Error EADDRINUSE received, exiting process without restarting process...");
process.exit(0)
}
});
class PairDropServer {
constructor() {
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
this._rooms = {}; // { roomId: peers[] }
this._roomSecrets = {}; // { pairKey: roomSecret }
this._keepAliveTimers = {};
console.log('PairDrop is running on port', port);
}
_onConnection(peer) {
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
this._keepAlive(peer);
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
});
// send displayName
this._send(peer, {
type: 'display-name',
message: {
displayName: peer.name.displayName,
deviceName: peer.name.deviceName,
peerId: peer.id,
peerIdHash: hasher.hashCodeSalted(peer.id)
}
});
}
_onMessage(sender, message) {
// Try to parse message
try {
message = JSON.parse(message);
} catch (e) {
return; // TODO: handle malformed JSON
}
switch (message.type) {
case 'disconnect':
this._onDisconnect(sender);
break;
case 'pong':
this._setKeepAliveTimerToNow(sender);
break;
case 'join-ip-room':
this._joinIpRoom(sender);
break;
case 'room-secrets':
this._onRoomSecrets(sender, message);
break;
case 'room-secrets-deleted':
this._onRoomSecretsDeleted(sender, message);
break;
case 'pair-device-initiate':
this._onPairDeviceInitiate(sender);
break;
case 'pair-device-join':
this._onPairDeviceJoin(sender, message);
break;
case 'pair-device-cancel':
this._onPairDeviceCancel(sender);
break;
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
break;
case 'create-public-room':
this._onCreatePublicRoom(sender);
break;
case 'join-public-room':
this._onJoinPublicRoom(sender, message);
break;
case 'leave-public-room':
this._onLeavePublicRoom(sender);
break;
case 'signal':
default:
this._signalAndRelay(sender, message);
}
}
_signalAndRelay(sender, message) {
const room = message.roomType === 'ip'
? sender.ip
: message.roomId;
// relay message to recipient
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
const recipient = this._rooms[room][message.to];
delete message.to;
// add sender
message.sender = {
id: sender.id,
rtcSupported: sender.rtcSupported
};
this._send(recipient, message);
}
}
_onDisconnect(sender) {
this._disconnect(sender);
}
_disconnect(sender) {
this._removePairKey(sender.pairKey);
sender.pairKey = null;
this._cancelKeepAlive(sender);
delete this._keepAliveTimers[sender.id];
this._leaveIpRoom(sender, true);
this._leaveAllSecretRooms(sender, true);
this._leavePublicRoom(sender, true);
sender.socket.terminate();
}
_onRoomSecrets(sender, message) {
if (!message.roomSecrets) return;
const roomSecrets = message.roomSecrets.filter(roomSecret => {
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
})
if (!roomSecrets) return;
this._joinSecretRooms(sender, roomSecrets);
}
_onRoomSecretsDeleted(sender, message) {
for (let i = 0; i<message.roomSecrets.length; i++) {
this._deleteSecretRoom(message.roomSecrets[i]);
}
}
_deleteSecretRoom(roomSecret) {
const room = this._rooms[roomSecret];
if (!room) return;
for (const peerId in room) {
const peer = room[peerId];
this._leaveSecretRoom(peer, roomSecret, true);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
}
}
_onPairDeviceInitiate(sender) {
let roomSecret = randomizer.getRandomString(256);
let pairKey = this._createPairKey(sender, roomSecret);
if (sender.pairKey) {
this._removePairKey(sender.pairKey);
}
sender.pairKey = pairKey;
this._send(sender, {
type: 'pair-device-initiated',
roomSecret: roomSecret,
pairKey: pairKey
});
this._joinSecretRoom(sender, roomSecret);
}
_onPairDeviceJoin(sender, message) {
if (sender.rateLimitReached()) {
this._send(sender, { type: 'join-key-rate-limit' });
return;
}
if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
this._send(sender, { type: 'pair-device-join-key-invalid' });
return;
}
const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
const creator = this._roomSecrets[message.pairKey].creator;
this._removePairKey(message.pairKey);
this._send(sender, {
type: 'pair-device-joined',
roomSecret: roomSecret,
peerId: creator.id
});
this._send(creator, {
type: 'pair-device-joined',
roomSecret: roomSecret,
peerId: sender.id
});
this._joinSecretRoom(sender, roomSecret);
this._removePairKey(sender.pairKey);
}
_onPairDeviceCancel(sender) {
const pairKey = sender.pairKey
if (!pairKey) return;
this._removePairKey(pairKey);
this._send(sender, {
type: 'pair-device-canceled',
pairKey: pairKey,
});
}
_onCreatePublicRoom(sender) {
let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
this._send(sender, {
type: 'public-room-created',
roomId: publicRoomId
});
this._joinPublicRoom(sender, publicRoomId);
}
_onJoinPublicRoom(sender, message) {
if (sender.rateLimitReached()) {
this._send(sender, { type: 'join-key-rate-limit' });
return;
}
if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
return;
}
this._leavePublicRoom(sender);
this._joinPublicRoom(sender, message.publicRoomId);
}
_onLeavePublicRoom(sender) {
this._leavePublicRoom(sender, true);
this._send(sender, { type: 'public-room-left' });
}
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
// notify all other peers
for (const peerId in this._rooms[oldRoomSecret]) {
const peer = this._rooms[oldRoomSecret][peerId];
this._send(peer, {
type: 'room-secret-regenerated',
oldRoomSecret: oldRoomSecret,
newRoomSecret: newRoomSecret,
});
peer.removeRoomSecret(oldRoomSecret);
}
delete this._rooms[oldRoomSecret];
}
_createPairKey(creator, roomSecret) {
let pairKey;
do {
// get randomInt until keyRoom not occupied
pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
} while (pairKey in this._roomSecrets)
this._roomSecrets[pairKey] = {
roomSecret: roomSecret,
creator: creator
}
return pairKey;
}
_removePairKey(roomKey) {
if (roomKey in this._roomSecrets) {
this._roomSecrets[roomKey].creator.roomKey = null
delete this._roomSecrets[roomKey];
}
}
_joinIpRoom(peer) {
this._joinRoom(peer, 'ip', peer.ip);
}
_joinSecretRoom(peer, roomSecret) {
this._joinRoom(peer, 'secret', roomSecret);
// add secret to peer
peer.addRoomSecret(roomSecret);
}
_joinPublicRoom(peer, publicRoomId) {
// prevent joining of 2 public rooms simultaneously
this._leavePublicRoom(peer);
this._joinRoom(peer, 'public-id', publicRoomId);
peer.publicRoomId = publicRoomId;
}
_joinRoom(peer, roomType, roomId) {
// roomType: 'ip', 'secret' or 'public-id'
if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
this._leaveRoom(peer, roomType, roomId);
}
// if room doesn't exist, create it
if (!this._rooms[roomId]) {
this._rooms[roomId] = {};
}
this._notifyPeers(peer, roomType, roomId);
// add peer to room
this._rooms[roomId][peer.id] = peer;
}
_leaveIpRoom(peer, disconnect = false) {
this._leaveRoom(peer, 'ip', peer.ip, disconnect);
}
_leaveSecretRoom(peer, roomSecret, disconnect = false) {
this._leaveRoom(peer, 'secret', roomSecret, disconnect)
//remove secret from peer
peer.removeRoomSecret(roomSecret);
}
_leavePublicRoom(peer, disconnect = false) {
if (!peer.publicRoomId) return;
this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
peer.publicRoomId = null;
}
_leaveRoom(peer, roomType, roomId, disconnect = false) {
if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
// remove peer from room
delete this._rooms[roomId][peer.id];
// delete room if empty and abort
if (!Object.keys(this._rooms[roomId]).length) {
delete this._rooms[roomId];
return;
}
// notify all other peers that remain in room that peer left
for (const otherPeerId in this._rooms[roomId]) {
const otherPeer = this._rooms[roomId][otherPeerId];
let msg = {
type: 'peer-left',
peerId: peer.id,
roomType: roomType,
roomId: roomId,
disconnect: disconnect
};
this._send(otherPeer, msg);
}
}
_notifyPeers(peer, roomType, roomId) {
if (!this._rooms[roomId]) return;
// notify all other peers that peer joined
for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
const otherPeer = this._rooms[roomId][otherPeerId];
let msg = {
type: 'peer-joined',
peer: peer.getInfo(),
roomType: roomType,
roomId: roomId
};
this._send(otherPeer, msg);
}
// notify peer about peers already in the room
const otherPeers = [];
for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
}
let msg = {
type: 'peers',
peers: otherPeers,
roomType: roomType,
roomId: roomId
};
this._send(peer, msg);
}
_joinSecretRooms(peer, roomSecrets) {
for (let i=0; i<roomSecrets.length; i++) {
this._joinSecretRoom(peer, roomSecrets[i])
}
}
_leaveAllSecretRooms(peer, disconnect = false) {
for (let i=0; i<peer.roomSecrets.length; i++) {
this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);
}
}
_send(peer, message) {
if (!peer) return;
if (this._wss.readyState !== this._wss.OPEN) return;
message = JSON.stringify(message);
peer.socket.send(message);
}
_keepAlive(peer) {
this._cancelKeepAlive(peer);
let timeout = 1000;
if (!this._keepAliveTimers[peer.id]) {
this._keepAliveTimers[peer.id] = {
timer: 0,
lastBeat: Date.now()
};
}
if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {
// Disconnect peer if unresponsive for 10s
this._disconnect(peer);
return;
}
this._send(peer, { type: 'ping' });
this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);
}
_cancelKeepAlive(peer) {
if (this._keepAliveTimers[peer.id]?.timer) {
clearTimeout(this._keepAliveTimers[peer.id].timer);
}
}
_setKeepAliveTimerToNow(peer) {
if (this._keepAliveTimers[peer.id]?.lastBeat) {
this._keepAliveTimers[peer.id].lastBeat = Date.now();
}
}
}
class Peer {
constructor(socket, request) {
// set socket
this.socket = socket;
// set remote ip
this._setIP(request);
// set peer id
this._setPeerId(request);
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
// set name
this._setName(request);
this.requestRate = 0;
this.roomSecrets = [];
this.roomKey = null;
this.publicRoomId = null;
}
rateLimitReached() {
// rate limit implementation: max 10 attempts every 10s
if (this.requestRate >= 10) {
return true;
}
this.requestRate += 1;
setTimeout(_ => this.requestRate -= 1, 10000);
return false;
}
_setIP(request) {
if (request.headers['cf-connecting-ip']) {
this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0];
} else if (request.headers['x-forwarded-for']) {
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
} else {
this.ip = request.connection.remoteAddress;
}
// remove the prefix used for IPv4-translated addresses
if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7);
let ipv6_was_localized = false;
if (ipv6_lcl && this.ip.includes(':')) {
this.ip = this.ip.split(':',ipv6_lcl).join(':');
ipv6_was_localized = true;
}
if (debugMode) {
console.debug("----DEBUGGING-PEER-IP-START----");
console.debug("remoteAddress:", request.connection.remoteAddress);
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
if (ipv6_was_localized)
console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment");
console.debug("PairDrop uses:", this.ip);
console.debug("IP is private:", this.ipIsPrivate(this.ip));
console.debug("if IP is private, '127.0.0.1' is used instead");
console.debug("----DEBUGGING-PEER-IP-END----");
}
// IPv4 and IPv6 use different values to refer to localhost
// put all peers on the same network as the server into the same room as well
if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {
this.ip = '127.0.0.1';
}
}
ipIsPrivate(ip) {
// if ip is IPv4
if (!ip.includes(":")) {
// 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255
return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip)
}
// else: ip is IPv6
const firstWord = ip.split(":").find(el => !!el); //get first not empty word
// The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff
if (/^fe[c-f][0-f]$/.test(firstWord))
return true;
// These days Unique Local Addresses (ULA) are used in place of Site Local.
// Range: fc00 - fcff
else if (/^fc[0-f]{2}$/.test(firstWord))
return true;
// Range: fd00 - fcff
else if (/^fd[0-f]{2}$/.test(firstWord))
return true;
// Link local addresses (prefixed with fe80) are not routable
else if (firstWord === "fe80")
return true;
// Discard Prefix
else if (firstWord === "100")
return true;
// Any other IP address is not Unique Local Address (ULA)
return false;
}
_setPeerId(request) {
const searchParams = new URL(request.url, "http://server").searchParams;
let peerId = searchParams.get("peer_id");
let peerIdHash = searchParams.get("peer_id_hash");
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
this.id = peerId;
} else {
this.id = crypto.randomUUID();
}
}
toString() {
return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>`
}
_setName(req) {
let ua = parser(req.headers['user-agent']);
let deviceName = '';
if (ua.os && ua.os.name) {
deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' ';
}
if (ua.device.model) {
deviceName += ua.device.model;
} else {
deviceName += ua.browser.name;
}
if(!deviceName)
deviceName = 'Unknown Device';
const displayName = uniqueNamesGenerator({
length: 2,
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: cyrb53(this.id)
})
this.name = {
model: ua.device.model,
os: ua.os.name,
browser: ua.browser.name,
type: ua.device.type,
deviceName,
displayName
};
}
getInfo() {
return {
id: this.id,
name: this.name,
rtcSupported: this.rtcSupported
}
}
static isValidUuid(uuid) {
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
}
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) {
if (!(roomSecret in this.roomSecrets)) {
this.roomSecrets.push(roomSecret);
}
}
removeRoomSecret(roomSecret) {
if (roomSecret in this.roomSecrets) {
delete this.roomSecrets[roomSecret];
}
}
}
const hasher = (() => {
let password;
return {
hashCodeSalted(salt) {
if (!password) {
// password is created on first call.
password = randomizer.getRandomString(128);
}
return crypto.createHash("sha3-512")
.update(password)
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
.digest("hex");
}
}
})()
const randomizer = (() => {
let charCodeLettersOnly = r => 65 <= r && r <= 90;
let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122;
return {
getRandomString(length, lettersOnly = false) {
const charCodeCondition = lettersOnly
? charCodeLettersOnly
: charCodeAllPrintableChars;
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
return charCodeCondition(r);
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
}
})()
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
new PairDropServer();
-28
View File
@@ -1,28 +0,0 @@
BSD 3-Clause License
Copyright (c) 2023, Gildas Lormeau
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-22
View File
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) Rich Tibbett
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+513 -173
View File
@@ -1,19 +1,19 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.10", "version": "1.9.1",
"lockfileVersion": 3, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.10", "version": "1.9.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.36",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.16.0" "ws": "^8.14.2"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
@@ -37,12 +37,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.4",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"destroy": "1.2.0", "destroy": "1.2.0",
@@ -50,7 +50,7 @@
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.11.0",
"raw-body": "2.5.2", "raw-body": "2.5.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
}, },
@@ -68,18 +68,12 @@
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0", "function-bind": "^1.1.1",
"es-errors": "^1.3.0", "get-intrinsic": "^1.0.2"
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -97,17 +91,17 @@
} }
}, },
"node_modules/content-type": { "node_modules/content-type": {
"version": "1.0.5", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -125,22 +119,6 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -171,25 +149,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -204,16 +163,16 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.1",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.5.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -245,17 +204,14 @@
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
"version": "7.3.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.1.tgz",
"integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", "integrity": "sha512-o5ye/a4EHCPQPju25Y4HChHybrCM9v37QtQDqXUDZGuD+HB7Cbu8ZhJP6/9RORcSNtkCpnEssa6oUgJgzc7ckQ==",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
}, },
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": { "peerDependencies": {
"express": "4 || 5 || ^5.0.0-beta.1" "express": "^4 || ^5"
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
@@ -292,62 +248,32 @@
} }
}, },
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "function-bind": "^1.1.1",
"function-bind": "^1.1.2", "has": "^1.0.3",
"has-proto": "^1.0.1", "has-symbols": "^1.0.3"
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gopd": { "node_modules/has": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"engines": { "dependencies": {
"node": ">= 0.4" "function-bind": "^1.1.1"
}, },
"funding": { "engines": {
"url": "https://github.com/sponsors/ljharb" "node": ">= 0.4.0"
} }
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
@@ -361,17 +287,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -476,12 +391,9 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -545,9 +457,9 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
@@ -624,39 +536,19 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.0.6", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": { "dependencies": {
"call-bind": "^1.0.7", "call-bind": "^1.0.0",
"es-errors": "^1.3.0", "get-intrinsic": "^1.0.2",
"get-intrinsic": "^1.2.4", "object-inspect": "^1.9.0"
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -691,9 +583,9 @@
} }
}, },
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.38", "version": "1.0.36",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
"integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -745,9 +637,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.17.1", "version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -764,5 +656,453 @@
} }
} }
} }
},
"dependencies": {
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
}
},
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"requires": {
"safe-buffer": "5.2.1"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"express-rate-limit": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.1.tgz",
"integrity": "sha512-o5ye/a4EHCPQPju25Y4HChHybrCM9v37QtQDqXUDZGuD+HB7Cbu8ZhJP6/9RORcSNtkCpnEssa6oUgJgzc7ckQ==",
"requires": {}
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"get-intrinsic": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"requires": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.4"
}
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"dependencies": {
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
}
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
}
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"ua-parser-js": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw=="
},
"unique-names-generator": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz",
"integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"ws": {
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"requires": {}
}
} }
} }
+7 -8
View File
@@ -1,21 +1,20 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.10", "version": "1.9.1",
"type": "module",
"description": "", "description": "",
"main": "server/index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node server/index.js", "start": "node index.js",
"start:prod": "node server/index.js --rate-limit --auto-restart" "start:prod": "node index.js --rate-limit --auto-restart"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.36",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.16.0" "ws": "^8.14.2"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
-1
View File
@@ -1 +0,0 @@
.pairdrop-cli-config
@@ -1 +0,0 @@
DOMAIN=https://pairdrop.net/
+76 -237
View File
@@ -1,9 +1,6 @@
#!/bin/bash #!/bin/bash
set -e set -e
# PairDrop version when this file was last changed
version="v1.10.4"
############################################################ ############################################################
# Help # # Help #
############################################################ ############################################################
@@ -15,12 +12,10 @@ help()
echo echo
echo "Usage:" echo "Usage:"
echo -e "Open PairDrop:\t\t$(basename "$0")" echo -e "Open PairDrop:\t\t$(basename "$0")"
echo -e "Send files:\t\t$(basename "$0") file1/directory1 (file2/directory2 file3/directory3 ...)" echo -e "Send files:\t\t$(basename "$0") file/directory"
echo -e "Send text:\t\t$(basename "$0") -t \"text\"" echo -e "Send text:\t\t$(basename "$0") -t \"text\""
echo -e "Specify domain:\t\t$(basename "$0") -d \"https://pairdrop.net/\"" echo -e "Specify domain:\t\t$(basename "$0") -d \"https://pairdrop.net/\""
echo -e "Show this help text:\t$(basename "$0") (-h|--help)" echo -e "Show this help text:\t$(basename "$0") (-h|--help)"
echo
echo "This pairdrop-cli version was released alongside ${version}"
} }
openPairDrop() openPairDrop()
@@ -41,7 +36,7 @@ openPairDrop()
elif [[ $OS == "WSL" || $OS == "WSL2" ]];then elif [[ $OS == "WSL" || $OS == "WSL2" ]];then
powershell.exe /c "Start-Process ${url}" powershell.exe /c "Start-Process ${url}"
else else
xdg-open "$url" > /dev/null 2>&1 xdg-open "$url"
fi fi
@@ -67,7 +62,7 @@ setOs()
specifyDomain() specifyDomain()
{ {
[[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo "Incorrect format. Specify domain like https://pairdrop.net/" && exit [[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo "Incorrect format. Specify domain like https://pairdrop.net/" && exit
echo "DOMAIN=${1}" > "$config_path" echo "DOMAIN=${1}" > "$CONFIGPATH"
echo -e "Domain is now set to:\n$1\n" echo -e "Domain is now set to:\n$1\n"
} }
@@ -92,228 +87,75 @@ sendText()
exit exit
} }
escapePSPath()
{
local path=$1
# escape '[' and ']' with grave accent (`) character
pathPS=${path//[/\`[}
pathPS=${pathPS//]/\`]}
# escape single quote (') with another single quote (')
pathPS=${pathPS//\'/\'\'}
# Convert GitHub bash path "/i/path" to Windows path "I:/path"
if [[ $pathPS == /* ]]; then
# Remove preceding slash
pathPS="${pathPS#/}"
# Convert drive letter to uppercase
driveLetter=$(echo "${pathPS::1}" | tr '[:lower:]' '[:upper:]')
# Put together absolute path as used in Windows
pathPS="${driveLetter}:${pathPS:1}"
fi
echo "$pathPS"
}
sendFiles() sendFiles()
{ {
params="base64zip=hash" params="base64zip=hash"
workingDir="$(pwd)" if [[ $1 == */ ]]; then
tmpDir="/tmp/pairdrop-cli-temp/" path="${1::-1}"
tmpDirPS="\$env:TEMP/pairdrop-cli-temp/"
index=0
directoryBaseNamesUnix=()
directoryPathsUnix=()
filePathsUnix=()
directoryCount=0
fileCount=0
pathsPS=""
#create tmp folder if it does not exist already
if [[ ! -d "$tmpDir" ]]; then
mkdir "$tmpDir"
fi
for arg in "$@"; do
echo "$arg"
[[ ! -e "$arg" ]] && echo "The given path $arg does not exist." && exit
# Remove trailing slash from directory
arg="${arg%/}"
# get absolute path and basename of file/directory
absolutePath=$(realpath "$arg")
baseName=$(basename "$absolutePath")
directoryPath=$(dirname "$absolutePath")
if [[ -d $absolutePath ]]; then
# is directory
((directoryCount+=1))
# add basename and directory path to arrays
directoryBaseNamesUnix+=("$baseName")
directoryPathsUnix+=("$directoryPath")
else
# is file
((fileCount+=1))
absolutePathUnix=$absolutePath
# append new path and separate paths with space
filePathsUnix+=("$absolutePathUnix")
fi
# Prepare paths for PowerShell on Windows
if [[ $OS == "Windows" ]];then
absolutePathPS=$(escapePSPath "$absolutePath")
# append new path and separate paths with commas
pathsPS+="'${absolutePathPS}', "
fi
# set fileNames on first loop
if [[ $index == 0 ]]; then
baseNameU=${baseName// /_}
# Prevent baseNameU being empty for hidden files by removing the preceding dot
if [[ $baseNameU == .* ]]; then
baseNameU=${baseNameU#.*}
fi
# only use trunk of basename "document.txt" -> "document"
baseNameTrunk=${baseNameU%.*}
# remove all special characters
zipName=${baseNameTrunk//[^a-zA-Z0-9_]/}
zipToSendAbs="${tmpDir}${zipName}_pairdrop.zip"
wrapperZipAbs="${tmpDir}${zipName}_pairdrop_wrapper.zip"
if [[ $OS == "Windows" ]];then
zipToSendAbsPS="${tmpDirPS}${zipName}_pairdrop.zip"
wrapperZipAbsPS="${tmpDirPS}${zipName}_pairdrop_wrapper.zip"
fi
fi
((index+=1)) # somehow ((index++)) stops the script
done
# Prepare paths for PowerShell on Windows
if [[ $OS == "Windows" ]];then
# remove trailing comma
pathsPS=${pathsPS%??}
fi
echo "Preparing ${fileCount} files and ${directoryCount} directories..."
# if arguments include files only -> zip files once so files it is unzipped by sending browser
# if arguments include directories -> wrap first zip in a second wrapper zip so that after unzip by sending browser a zip file is sent to receiver
#
# Preferred zip structure:
# pairdrop "d1/d2/d3/f1" "../../d4/d5/d6/f2" "d7/" "../d8/" "f5"
# zip structure: pairdrop.zip
# |-f1
# |-f2
# |-d7/
# |-d8/
# |-f5
# -> truncate (relative) paths but keep directories
[[ -e "$zipToSendAbs" ]] && echo "Cannot overwrite $zipToSendAbs. Please remove first." && exit
if [[ $OS == "Windows" ]];then
# Powershell does preferred zip structure natively
powershell.exe -Command "Compress-Archive -Path ${pathsPS} -DestinationPath ${zipToSendAbsPS}"
else else
# Workaround needed to create preferred zip structure on unix systems path=$1
# Create zip file with all single files by junking the path fi
if [[ $fileCount != 0 ]]; then zipPath="${path}_pairdrop.zip"
zip -q -b /tmp/ -j -0 -r "$zipToSendAbs" "${filePathsUnix[@]}" zipPath=${zipPath// /_}
[[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit
if [[ -d $path ]]; then
zipPathTemp="${path}_pairdrop_temp.zip"
[[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit
echo "Processing directory..."
# Create zip files temporarily to send directory
if [[ $OS == "Windows" ]];then
powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath}"
echo "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}"
powershell.exe -Command "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}"
else
zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
fi fi
# Add directories recursively to zip file if [[ $OS == "Mac" ]];then
index=0 hash=$(base64 -i "$zipPathTemp")
while [[ $index < $directoryCount ]]; do else
# workaround to keep directory name but junk the rest of the paths hash=$(base64 -w 0 "$zipPathTemp")
fi
# cd to path above directory # remove temporary temp file
cd "${directoryPathsUnix[index]}" rm "$zipPathTemp"
else
echo "Processing file..."
# add directory to zip without junking the path # Create zip file temporarily to send file
zip -q -b /tmp/ -0 -u -r "$zipToSendAbs" "${directoryBaseNamesUnix[index]}"
# cd back to working directory
cd "$workingDir"
((index+=1)) # somehow ((index++)) stops the script
done
fi
# If directories are included send as zip
# -> Create additional zip wrapper which will be unzipped by the sending browser
if [[ "$directoryCount" != 0 ]]; then
echo "Bundle as ZIP file..."
# Prevent filename from being absolute zip path by "cd"ing to directory before zipping
zipToSendDirectory=$(dirname "$zipToSendAbs")
zipToSendBaseName=$(basename "$zipToSendAbs")
cd "$zipToSendDirectory"
[[ -e "$wrapperZipAbs" ]] && echo "Cannot overwrite $wrapperZipAbs. Please remove first." && exit
if [[ $OS == "Windows" ]];then if [[ $OS == "Windows" ]];then
powershell.exe -Command "Compress-Archive -Path ${zipToSendBaseName} -DestinationPath ${wrapperZipAbsPS} -CompressionLevel Optimal" powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath} -CompressionLevel Optimal"
else else
zip -q -b /tmp/ -0 "$wrapperZipAbs" "$zipToSendBaseName" zip -q -b /tmp/ "$zipPath" "$path"
fi
if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPath")
else
hash=$(base64 -w 0 "$zipPath")
fi fi
cd "$workingDir"
# remove inner zip file and set wrapper as zipToSend (do not differentiate between OS as this is done via Git Bash on Windows)
rm "$zipToSendAbs"
zipToSendAbs=$wrapperZipAbs
fi fi
# base64 encode zip file # remove temporary temp file
if [[ $OS == "Mac" ]];then rm "$zipPath"
hash=$(base64 -i "$zipToSendAbs")
else
hash=$(base64 -w 0 "$zipToSendAbs")
fi
# remove zip file (do not differentiate between OS as this is done via Git Bash on Windows) if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then
rm "$zipToSendAbs"
if [[ $(echo -n "$hash" | wc -m) -gt 1000 ]];then
params="base64zip=paste" params="base64zip=paste"
# Copy $hash to clipboard
if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then
echo -n "$hash" | clip.exe echo -n "$hash" | clip.exe
elif [[ $OS == "Mac" ]];then elif [[ $OS == "Mac" ]];then
echo -n "$hash" | pbcopy echo -n "$hash" | pbcopy
elif [ -n "$WAYLAND_DISPLAY" ]; then
# Wayland
if ! command -v wl-copy &> /dev/null; then
echo -e "You need to install 'wl-copy' to send bigger filePathsUnix from cli"
echo "Try: sudo apt install wl-clipboard"
exit 1
fi
# Workaround to prevent use of Pipe which has a max letter limit
echo -n "$hash" > /tmp/pairdrop-cli-temp/pairdrop_hash_temp
wl-copy < /tmp/pairdrop-cli-temp/pairdrop_hash_temp
rm /tmp/pairdrop-cli-temp/pairdrop_hash_temp
else else
# X11 (echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli"
if ! command -v xclip &> /dev/null; then
echo -e "You need to install 'xclip' to send bigger filePathsUnix from cli"
echo "Try: sudo apt install xclip"
exit 1
fi
echo -n "$hash" | xclip -sel c
fi fi
hash= hash=
fi fi
openPairDrop openPairDrop
exit exit
} }
@@ -323,35 +165,31 @@ sendFiles()
# Main program # # Main program #
############################################################ ############################################################
############################################################ ############################################################
script_path="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
pushd . > '/dev/null'; pushd . > '/dev/null';
script_path="${BASH_SOURCE[0]:-$0}"; SCRIPTPATH="${BASH_SOURCE[0]:-$0}";
while [ -h "$script_path" ]; while [ -h "$SCRIPTPATH" ];
do do
cd "$( dirname -- "$script_path"; )"; cd "$( dirname -- "$SCRIPTPATH"; )";
script_path="$( readlink -f -- "$script_path"; )"; SCRIPTPATH="$( readlink -f -- "$SCRIPTPATH"; )";
done done
cd "$( dirname -- "$script_path"; )" > '/dev/null'; cd "$( dirname -- "$SCRIPTPATH"; )" > '/dev/null';
script_path="$( pwd; )"; SCRIPTPATH="$( pwd; )";
popd > '/dev/null'; popd > '/dev/null';
config_path="${script_path}/.pairdrop-cli-config" CONFIGPATH="${SCRIPTPATH}/.pairdrop-cli-config"
# If config file does not exist, try to create it. If it fails log error message and exit [ ! -f "$CONFIGPATH" ] &&
[ ! -f "$config_path" ] && specifyDomain "https://pairdrop.net/" &&
specifyDomain "https://pairdrop.net/" && [ ! -f "$CONFIGPATH" ] &&
[ ! -f "$config_path" ] && echo "Could not create config file. Add 'DOMAIN=https://pairdrop.net/' to a file called .pairdrop-cli-config in the same file as this 'pairdrop' bash file"
echo "Could not create config file. Add 'DOMAIN=https://pairdrop.net/' to a file called .pairdrop-cli-config in the same file as this 'pairdrop' bash file (${script_path})" &&
exit
# Read config variables [ ! -f "$CONFIGPATH" ] || export "$(grep -v '^#' "$CONFIGPATH" | xargs)"
export "$(grep -v '^#' "$config_path" | xargs)"
setOs setOs
############################################################ ############################################################
# Process the input options. Add options as needed. # # Process the input options. Add options as needed. #
############################################################ ############################################################
@@ -360,23 +198,24 @@ setOs
[[ $# -eq 0 ]] && openPairDrop && exit [[ $# -eq 0 ]] && openPairDrop && exit
# display help and exit if first argument is "--help" or more than 2 arguments are given # display help and exit if first argument is "--help" or more than 2 arguments are given
[ "$1" == "--help" ] && help && exit [ "$1" == "--help" ] || [[ $# -gt 2 ]] && help && exit
while getopts "d:ht:*" option; do while getopts "d:ht:*" option; do
case $option in case $option in
d) # specify domain - show help and exit if too many arguments d) # specify domain
[[ $# -gt 2 ]] && help && exit specifyDomain "$2"
specifyDomain "$2" exit;;
exit;; t) # Send text
t) # Send text - show help and exit if too many arguments sendText
[[ $# -gt 2 ]] && help && exit exit;;
sendText h | ?) # display help and exit
exit;; help
h | ?) # display help and exit exit;;
help esac
exit;;
esac
done done
# Send file(s) # Send file(s)
sendFiles "$@" # display help and exit if 2 arguments are given or if file does not exist
[[ $# -eq 2 ]] || [[ ! -a $1 ]] && help && exit
sendFiles "$1"
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P )
cd "$parent_path" || exit
./pairdrop "$@"
Binary file not shown.
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# Initialize an array
lines=()
# Read each line into the array
while IFS= read -r line; do
lines+=("$line")
done <<< "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS"
# Get the length of the array
length=${#lines[@]}
# Remove the last entry
unset 'lines[length-1]'
pairdrop "${lines[@]}"
-3
View File
@@ -1,3 +0,0 @@
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
& "$scriptDir\pairdrop.sh" $args
-93
View File
@@ -1,93 +0,0 @@
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-100
View File
@@ -1,100 +0,0 @@
Open Sans Variable Font
=======================
This download contains Open Sans as both variable fonts and static fonts.
Open Sans is a variable font with these axes:
wdth
wght
This means all the styles are contained in these files:
OpenSans-VariableFont_wdth,wght.ttf
OpenSans-Italic-VariableFont_wdth,wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Open Sans:
static/OpenSans_Condensed-Light.ttf
static/OpenSans_Condensed-Regular.ttf
static/OpenSans_Condensed-Medium.ttf
static/OpenSans_Condensed-SemiBold.ttf
static/OpenSans_Condensed-Bold.ttf
static/OpenSans_Condensed-ExtraBold.ttf
static/OpenSans_SemiCondensed-Light.ttf
static/OpenSans_SemiCondensed-Regular.ttf
static/OpenSans_SemiCondensed-Medium.ttf
static/OpenSans_SemiCondensed-SemiBold.ttf
static/OpenSans_SemiCondensed-Bold.ttf
static/OpenSans_SemiCondensed-ExtraBold.ttf
static/OpenSans-Light.ttf
static/OpenSans-Regular.ttf
static/OpenSans-Medium.ttf
static/OpenSans-SemiBold.ttf
static/OpenSans-Bold.ttf
static/OpenSans-ExtraBold.ttf
static/OpenSans_Condensed-LightItalic.ttf
static/OpenSans_Condensed-Italic.ttf
static/OpenSans_Condensed-MediumItalic.ttf
static/OpenSans_Condensed-SemiBoldItalic.ttf
static/OpenSans_Condensed-BoldItalic.ttf
static/OpenSans_Condensed-ExtraBoldItalic.ttf
static/OpenSans_SemiCondensed-LightItalic.ttf
static/OpenSans_SemiCondensed-Italic.ttf
static/OpenSans_SemiCondensed-MediumItalic.ttf
static/OpenSans_SemiCondensed-SemiBoldItalic.ttf
static/OpenSans_SemiCondensed-BoldItalic.ttf
static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
static/OpenSans-LightItalic.ttf
static/OpenSans-Italic.ttf
static/OpenSans-MediumItalic.ttf
static/OpenSans-SemiBoldItalic.ttf
static/OpenSans-BoldItalic.ttf
static/OpenSans-ExtraBoldItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

+137 -328
View File
@@ -5,17 +5,17 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config --> <!-- Web App Config -->
<title>PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.</title> <title>PairDrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6"> <meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light"> <meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="no">
<meta name="apple-mobile-web-app-title" content="PairDrop"> <meta name="apple-mobile-web-app-title" content="PairDrop">
<meta name="application-name" content="PairDrop"> <meta name="application-name" content="PairDrop">
<!-- Descriptions --> <!-- Descriptions -->
<meta name="description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup."> <meta name="description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<meta name="keywords" content="File, Transfer, Share, Peer2Peer"> <meta name="keywords" content="File, Transfer, Share, Peer2Peer">
<meta name="author" content="schlagmichdoch"> <meta name="author" content="RobinLinus">
<meta property="og:title" content="PairDrop"> <meta property="og:title" content="PairDrop">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="https://pairdrop.net/"> <meta property="og:url" content="https://pairdrop.net/">
@@ -28,20 +28,18 @@
<link rel="icon" sizes="96x96" href="images/favicon-96x96.png"> <link rel="icon" sizes="96x96" href="images/favicon-96x96.png">
<link rel="shortcut icon" href="images/favicon-96x96.png"> <link rel="shortcut icon" href="images/favicon-96x96.png">
<link rel="apple-touch-icon" href="images/apple-touch-icon.png"> <link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="apple-touch-icon-precomposed" href="images/apple-touch-icon.png">
<meta name="msapplication-TileImage" content="images/mstile-150x150.png"> <meta name="msapplication-TileImage" content="images/mstile-150x150.png">
<link rel="fluid-icon" type="image/png" href="images/android-chrome-192x192.png"> <link rel="fluid-icon" type="image/png" href="images/android-chrome-192x192.png">
<meta name="twitter:image" content="images/logo_transparent_512x512.png"> <meta name="twitter:image" content="images/logo_transparent_512x512.png">
<meta property="og:image" content="images/logo_transparent_512x512.png"> <meta property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources --> <!-- Resources -->
<link rel="preload" href="lang/en.json" as="fetch"> <link rel="preload" href="lang/en.json" as="fetch">
<link rel="preload" href="fonts/OpenSans/static/OpenSans-Medium.ttf" as="font" type="font/ttf" crossorigin> <link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="styles/styles-main.css">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body translate="no"> <body translate="no">
<header class="row-reverse wrap opacity-0"> <header class="row-reverse opacity-0">
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label"> <a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label">
<svg class="icon"> <svg class="icon">
<use xlink:href="#info-outline"></use> <use xlink:href="#info-outline"></use>
@@ -96,79 +94,39 @@
<use xlink:href="#public-room-icon"></use> <use xlink:href="#public-room-icon"></use>
</svg> </svg>
</div> </div>
<div id="expand" class="icon-button" data-i18n-key="header.expand" data-i18n-attrs="title" hidden> <div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden></div>
<svg class="icon">
<use xlink:href="#caret"></use>
</svg>
</div>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center" class="opacity-0"> <div id="center" class="opacity-0">
<!-- Peers --> <!-- Peers -->
<x-peers class="center grow-5"></x-peers> <div class="x-peers-filler"></div>
<x-no-peers class="center grow fade-in no-animation-on-load" data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg"> <x-peers class="center"></x-peers>
<x-no-peers class="no-animation-on-load" data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg">
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text"></h2> <h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text"></h2>
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text"></div> <div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text"></div>
</x-no-peers> </x-no-peers>
<x-instructions class="grow fade-in" data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"></x-instructions> <x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg">
<div class="shr-panel panel column" hidden> <p id="paste-filename"></p>
<div class="row"> </x-instructions>
<div class="thumb center">
<div class="text-thumb row" hidden>
<svg>
<use xlink:href="#font"></use>
</svg>
<svg>
<use xlink:href="#i-cursor"></use>
</svg>
</div>
<div class="file-thumb" hidden>
<svg>
<use xlink:href="#file"></use>
</svg>
</div>
<div class="image-thumb" hidden></div>
</div>
<div class="share-descriptor column p-1">
<span class="descriptor-item"></span>
<span class="descriptor-other" hidden></span>
</div>
</div>
<div class="center btn-row wrap">
<div class="cancel-btn btn btn-small btn-rounded btn-dark text-white" data-i18n-key="header.cancel-share-mode" data-i18n-attrs="text"></div>
<div class="edit-btn btn btn-small btn-rounded btn-dark text-white" data-i18n-key="header.edit-share-mode" data-i18n-attrs="text" hidden></div>
</div>
</div>
<div id="websocket-fallback" class="text-center" hidden>
<span data-i18n-key="footer.traffic" data-i18n-attrs="text"></span>
<span data-i18n-key="footer.routed" data-i18n-attrs="text"></span>
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text"></span>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
<footer class="column opacity-0"> <footer class="column opacity-0">
<svg class="icon logo"> <svg class="icon logo">
<defs> <use xlink:href="#wifi-tethering"></use>
<linearGradient id="primaryGradient" gradientTransform="rotate(90)">
<stop offset="0%" class="start-color" />
<stop offset="100%" class="stop-color" />
</linearGradient>
</defs>
<use xlink:href="#wifi-tethering" style="fill: url(#primaryGradient);"></use>
</svg> </svg>
<div class="column"> <div class="column">
<div class="known-as-wrapper"> <div class="known-as-wrapper">
<span data-i18n-key="footer.known-as" data-i18n-attrs="text"></span> <span data-i18n-key="footer.known-as" data-i18n-attrs="text"></span>
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div> <div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" placeholder="Loading..." autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg class="icon edit-pen"> <svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon"></use> <use xlink:href="#edit-pen-icon"></use>
</svg> </svg>
</div> </div>
<div class="discovery-wrapper panel border row"> <div class="discovery-wrapper row">
<div class="row center"> <div class="row center">
<span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span> <span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span>
</div> </div>
<div class="row center wrap"> <div class="row center">
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span> <span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span> <span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span> <span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
@@ -180,139 +138,77 @@
<x-dialog id="language-select-dialog"> <x-dialog id="language-select-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2> <h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2>
</div> </div>
<div class="language-buttons p2"> <div class="language-buttons">
<button class="btn fw wrap"> <button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></button>
<span data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></span> <button class="button fw" value="ar">
</button>
<button class="btn fw wrap" value="ar">
<span>العربية</span> <span>العربية</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Arabic)</span> <span>(Arabic)</span>
</button> </button>
<button class="btn fw wrap" value="be"> <button class="button fw" value="de">
<span>беларуская</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Belarusian)</span>
</button>
<button class="btn fw wrap" value="ca">
<span>Català</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Catalan)</span>
</button>
<button class="btn fw wrap" value="cs">
<span>Čeština</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Czech)</span>
</button>
<button class="btn fw wrap" value="da">
<span>Dansk</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Danish)</span>
</button>
<button class="btn fw wrap" value="de">
<span>Deutsch</span> <span>Deutsch</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(German)</span> <span>(German)</span>
</button> </button>
<button class="btn fw wrap" value="en"> <button class="button fw" value="en">
<span>English</span> <span>English</span>
</button> </button>
<button class="btn fw wrap" value="es"> <button class="button fw" value="fr">
<span>Español</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Spanish)</span>
</button>
<button class="btn fw wrap" value="fr">
<span>Français</span> <span>Français</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(French)</span> <span>(French)</span>
</button> </button>
<button class="btn fw wrap" value="id"> <button class="button fw" value="id">
<span>Bahasa Indonesia</span> <span>Bahasa Indonesia</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Indonesian)</span> <span>(Indonesian)</span>
</button> </button>
<button class="btn fw wrap" value="it"> <button class="button fw" value="it">
<span>Italiano</span> <span>Italiano</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Italian)</span> <span>(Italian)</span>
</button> </button>
<button class="btn fw wrap" value="he"> <button class="button fw" value="nl">
<span>עִבְרִית</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Hebrew)</span>
</button>
<button class="btn fw wrap" value="kn">
<span>ಕನ್ನಡ</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Kannada)</span>
</button>
<button class="btn fw wrap" value="hu">
<span>Magyar</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Hungarian)</span>
</button>
<button class="btn fw wrap" value="nl">
<span>Nederlands</span> <span>Nederlands</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Dutch)</span> <span>(Dutch)</span>
</button> </button>
<button class="btn fw wrap" value="nb"> <button class="button fw" value="nb">
<span>Norsk</span> <span>Norsk</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Norwegian)</span> <span>(Norwegian)</span>
</button> </button>
<button class="btn fw wrap" value="pl"> <button class="button fw" value="ro">
<span>Polski</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Polish)</span>
</button>
<button class="btn fw wrap" value="pt-BR">
<span>Português do Brasil</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Brazilian Portuguese)</span>
</button>
<button class="btn fw wrap" value="ro">
<span>Română</span> <span>Română</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Romanian)</span> <span>(Romanian)</span>
</button> </button>
<button class="btn fw wrap" value="ru"> <button class="button fw" value="ru">
<span>Русский язык</span> <span>Русский язык</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Russian)</span> <span>(Russian)</span>
</button> </button>
<button class="btn fw wrap" value="tr"> <button class="button fw" value="es">
<span>Türkçe</span> <span>Español</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Turkish)</span> <span>(Spanish)</span>
</button> </button>
<button class="btn fw wrap" value="uk"> <button class="button fw" value="zh-CN">
<span>Українська</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Ukrainian)</span>
</button>
<button class="btn fw wrap" value="zh-CN">
<span>中文</span> <span>中文</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Simplified Chinese)</span> <span>(Chinese)</span>
</button> </button>
<button class="btn fw wrap" value="zh-TW"> <button class="button fw" value="ja">
<span>漢語</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Traditional Chinese)</span>
</button>
<button class="btn fw wrap" value="ja">
<span>日本語</span> <span>日本語</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>-</span>
<span>(Japanese)</span> <span>(Japanese)</span>
</button> </button>
</div> </div>
<div class="center row-reverse btn-row wrap"> <div class="center row-reverse button-row">
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -322,12 +218,12 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text"></h2> <h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text"></h2>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column"> <div class="column">
<div class="center key-qr-code pointer" data-i18n-key="dialogs.pair-devices-qr-code" data-i18n-attrs="title"></div> <div class="center key-qr-code" data-i18n-key="dialogs.pair-devices-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr">000 000</h1> <h1 class="center key" dir="ltr">000 000</h1>
<p class="center text-center key-instructions"> <p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text"></span> <span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text"></span>
@@ -341,8 +237,8 @@
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span> <span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div> </div>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column fw"> <div class="column">
<div class="input-key-container six-chars" dir="ltr"> <div class="input-key-container six-chars" dir="ltr">
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled> <input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled> <input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
@@ -354,9 +250,9 @@
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text"></p> <p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text"></p>
</div> </div>
</div> </div>
<div class="btn-row row-reverse wrap"> <div class="button-row row-reverse">
<button class="btn btn-rounded btn-grey" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled></button> <button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled></button>
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button> <button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -367,8 +263,8 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text"></h2> <h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text"></h2>
</div> </div>
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty"></div> <div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty"></div>
<div class="font-subheading center"> <div class="font-subheading center">
@@ -378,8 +274,8 @@
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text"></span> <span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text"></span>
</p> </p>
</div> </div>
<div class="center row-reverse btn-row wrap"> <div class="center row-reverse button-row">
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -390,12 +286,14 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center p-2">
<div class="column"> <div class="column">
<div class="center key-qr-code pointer" data-i18n-key="dialogs.public-room-qr-code" data-i18n-attrs="title"></div> <h2 class="center" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row center">
<div class="column">
<div class="center key-qr-code" data-i18n-key="dialogs.public-room-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr"></h1> <h1 class="center key" dir="ltr"></h1>
<p class="center text-center key-instructions"> <p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text"></span> <span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text"></span>
@@ -409,8 +307,8 @@
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span> <span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div> </div>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column fw"> <div class="column">
<div class="input-key-container" dir="ltr"> <div class="input-key-container" dir="ltr">
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled> <input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled> <input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
@@ -421,12 +319,10 @@
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text"></p> <p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text"></p>
</div> </div>
</div> </div>
<div class="center row-reverse btn-row wrap"> <div class="center row-reverse button-row">
<div class="row-reverse wrap grow-2"> <button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button>
<button class="btn btn-rounded btn-grey" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="btn btn-rounded btn-grey leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button> <button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button>
</div>
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -436,10 +332,12 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title"></h2> <div class="column">
<h2 class="center"></h2>
</div>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column center file-description"> <div class="column center file-description">
<div> <div>
<span class="display-name badge"></span> <span class="display-name badge"></span>
@@ -455,9 +353,9 @@
</div> </div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse center btn-row wrap"> <div class="row-reverse center button-row">
<button id="accept-request" class="btn btn-rounded btn-grey" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus disabled></button> <button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus></button>
<button id="decline-request" class="btn btn-rounded btn-grey" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button> <button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -466,10 +364,12 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title"></h2> <div class="column">
<h2 class="center"></h2>
</div>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column center file-description"> <div class="column center file-description">
<div> <div>
<span class="display-name badge"></span> <span class="display-name badge"></span>
@@ -485,10 +385,10 @@
</div> </div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse center btn-row wrap"> <div class="row-reverse center button-row">
<button id="share-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button> <button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled></button> <button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus></button>
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -498,10 +398,12 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2> <div class="column">
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2>
</div>
</div> </div>
<div class="row center p-2 display-name-wrapper"> <div class="row center display-name-wrapper">
<div class="column"> <div class="column">
<div class="text-center"> <div class="text-center">
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text"></span> <span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text"></span>
@@ -509,14 +411,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row p-2"> <div class="row">
<div class="column fw"> <div class="column fw">
<div class="fw textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title placeholder" autofocus contenteditable></div> <div id="text-input" class="textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
</div> </div>
</div> </div>
<div class="btn-row row-reverse wrap"> <div class="button-row row-reverse">
<button class="btn btn-rounded btn-grey" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled></button> <button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled></button>
<button class="btn btn-rounded btn-grey" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button> <button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -526,55 +428,23 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <div class="row center">
<h2 class="dialog-title" class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2> <h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2>
</div> </div>
<div class="row center p-2 display-name-wrapper"> <div class="row center">
<div class="text-center"> <div class="text-center">
<span class="display-name badge"></span> <span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span> <span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div> </div>
</div> </div>
<div class="row center p-2"> <div class="row center">
<div class="column fw"> <div class="column fw">
<div id="text" class="textarea"></div> <div id="text" class="textarea fw"></div>
</div> </div>
</div> </div>
<div class="row-reverse center btn-row wrap"> <div class="row-reverse center button-row">
<button id="copy" class="btn btn-rounded btn-grey" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text"></button> <button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text"></button>
<button id="close" class="btn btn-rounded btn-grey" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text"></button> <button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Share Text Dialog -->
<x-dialog id="share-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" data-i18n-key="dialogs.share-text-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center p-2 pb-0">
<div class="column">
<div class="text-center">
<span data-i18n-key="dialogs.share-text-subtitle" data-i18n-attrs="text"></span>
</div>
</div>
</div>
<div class="row p-2">
<div class="column fw">
<div class="fw textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title placeholder" contenteditable></div>
</div>
</div>
<div class="row p-2 center">
<span class="mx-1" data-i18n-key="dialogs.share-text-checkbox" data-i18n-attrs="text"></span>
<label class="pointer switch mx-1">
<input type="checkbox">
<div class="slider round"></div>
</label>
</div>
<div class="btn-row row-reverse wrap">
<button class="btn btn-rounded btn-grey" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.approve" data-i18n-attrs="text" autofocus disabled></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -583,29 +453,17 @@
<x-dialog id="base64-paste-dialog"> <x-dialog id="base64-paste-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p-2"> <button class="button center" id="base64-paste-btn" title="Paste"></button>
<h2 class="dialog-title"></h2> <div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
</div> <div class="row-reverse center button-row">
<div class="row p-2"> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="btn btn-rounded btn-grey center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" title="CMD/⌘ + V" contenteditable hidden></div>
</div>
<div class="row-reverse center btn-row wrap">
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
</x-dialog> </x-dialog>
<!-- Toast --> <!-- Toast -->
<div class="toast-container full center"> <div class="toast-container full center">
<x-toast id="toast" shadow="1"> <x-toast id="toast" class="row center" shadow="1"></x-toast>
<span class="center text-center"></span>
<div class="icon-button" data-i18n-key="dialogs.close-toast" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#close-icon"></use>
</svg>
</div>
</x-toast>
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
@@ -622,7 +480,7 @@
</svg> </svg>
<div class="title-wrapper" dir="ltr"> <div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.10.10</div> <div class="font-subheading">v1.9.1</div>
</div> </div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row"> <div class="row">
@@ -631,36 +489,16 @@
<use xlink:href="#github"></use> <use xlink:href="#github"></use>
</svg> </svg>
</a> </a>
<a class="icon-button" id="donation-btn" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title"> <a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
<svg class="icon"> <svg class="icon">
<use xlink:href="#donation"></use> <use xlink:href="#monetarization"></use>
</svg> </svg>
</a> </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" 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"> <svg class="icon">
<use xlink:href="#twitter"></use> <use xlink:href="#twitter"></use>
</svg> </svg>
</a> </a>
<a class="icon-button" id="mastodon-btn" target="_blank" rel="noreferrer" data-i18n-key="about.mastodon" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#mastodon"></use>
</svg>
</a>
<a class="icon-button" id="bluesky-btn" target="_blank" rel="noreferrer" data-i18n-key="about.bluesky" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#bluesky"></use>
</svg>
</a>
<a class="icon-button" id="custom-btn" target="_blank" rel="noreferrer" data-i18n-key="about.custom" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#custom"></use>
</svg>
</a>
<a class="icon-button" id="privacypolicy-btn" target="_blank" rel="noreferrer" data-i18n-key="about.privacypolicy" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#privacypolicy"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title"> <a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
<svg class="icon"> <svg class="icon">
<use xlink:href="#help-outline"></use> <use xlink:href="#help-outline"></use>
@@ -700,25 +538,23 @@
<symbol id="github"> <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> <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</symbol> </symbol>
<symbol id="notifications"> <g id="notifications">
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"></path> <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"></path>
</symbol> </g>
<symbol id="homescreen"> <symbol id="homescreen">
<path fill="none" d="M0 0h24v24H0V0z"></path> <path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path> <path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path> <path fill="none" d="M0 0h24v24H0V0z"></path>
</symbol> </symbol>
<symbol id="donation"> <symbol id="monetarization">
<path d="M0 0h24v24H0z" fill="none"></path> <path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path>
</symbol> </symbol>
<symbol id="icon-theme-auto" viewBox="-54 -54 620 620"> <symbol id="icon-theme-auto" viewBox="0 0 24 24">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"></path></svg>
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"></path>
</symbol> </symbol>
<symbol id="icon-theme-light" viewBox="-54 -54 620 620"> <symbol id="icon-theme-light" viewBox="0 0 24 24">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"></path></svg>
<path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"></path>
</symbol> </symbol>
<symbol id="icon-theme-dark" viewBox="0 0 24 24"> <symbol id="icon-theme-dark" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"></rect><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path> <rect fill="none" height="24" width="24"></rect><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path>
@@ -745,44 +581,17 @@
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"></path> <path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"></path>
</symbol> </symbol>
<symbol id="i-cursor" viewBox="-180 0 640 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M.1 29.3C-1.4 47 11.7 62.4 29.3 63.9l8 .7C70.5 67.3 96 95 96 128.3V224H64c-17.7 0-32 14.3-32 32s14.3 32 32 32H96v95.7c0 33.3-25.5 61-58.7 63.8l-8 .7C11.7 449.6-1.4 465 .1 482.7s16.9 30.7 34.5 29.2l8-.7c34.1-2.8 64.2-18.9 85.4-42.9c21.2 24 51.2 40.1 85.4 42.9l8 .7c17.6 1.5 33.1-11.6 34.5-29.2s-11.6-33.1-29.2-34.5l-8-.7C185.5 444.7 160 417 160 383.7V288h32c17.7 0 32-14.3 32-32s-14.3-32-32-32H160V128.3c0-33.3 25.5-61 58.7-63.8l8-.7c17.6-1.5 30.7-16.9 29.2-34.5S239-1.4 221.3 .1l-8 .7C179.2 3.6 149.2 19.7 128 43.7c-21.2-24-51.2-40-85.4-42.9l-8-.7C17-1.4 1.6 11.7 .1 29.3z"></path>
</symbol>
<symbol id="font" viewBox="-100 0 640 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M254 52.8C249.3 40.3 237.3 32 224 32s-25.3 8.3-30 20.8L57.8 416H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32h-1.8l18-48H303.8l18 48H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H390.2L254 52.8zM279.8 304H168.2L224 155.1 279.8 304z"></path>
</symbol>
<symbol id="file" viewBox="-130 0 650 530">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M320 464c8.8 0 16-7.2 16-16V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320zM0 64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64z"></path>
</symbol>
<symbol id="caret" viewBox="0 0 320 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"></path>
</symbol>
<symbol id="mastodon" 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="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>
<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.-->
<path d="M418.4 157.9c35.3-8.3 61.6-40 61.6-77.9c0-44.2-35.8-80-80-80c-43.4 0-78.7 34.5-80 77.5L136.2 151.1C121.7 136.8 101.9 128 80 128c-44.2 0-80 35.8-80 80s35.8 80 80 80c12.2 0 23.8-2.7 34.1-7.6L259.7 407.8c-2.4 7.6-3.7 15.8-3.7 24.2c0 44.2 35.8 80 80 80s80-35.8 80-80c0-27.7-14-52.1-35.4-66.4l37.8-207.7zM156.3 232.2c2.2-6.9 3.5-14.2 3.7-21.7l183.8-73.5c3.6 3.5 7.4 6.7 11.6 9.5L317.6 354.1c-5.5 1.3-10.8 3.1-15.8 5.5L156.3 232.2z"></path>
</symbol>
<symbol id="privacypolicy" 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.-->
<path d="M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8V444.8C394 378 431.1 230.1 432 141.4L256 66.8l0 0z"></path>
</symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js" defer></script> <script src="scripts/localization.js"></script>
<script src="scripts/persistent-storage.js" defer></script> <script src="scripts/theme.js"></script>
<script src="scripts/ui-main.js" defer></script> <script src="scripts/network.js"></script>
<script src="scripts/main.js" defer></script> <script src="scripts/ui.js"></script>
<script src="scripts/util.js"></script>
<script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script>
<!-- Sounds --> <!-- Sounds -->
<audio id="blop" autobuffer="true"> <audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg"> <source src="sounds/blop.mp3" type="audio/mpeg">
+36 -37
View File
@@ -10,7 +10,7 @@
"paired-devices": "بواسطة الأجهزة المقترنة", "paired-devices": "بواسطة الأجهزة المقترنة",
"on-this-network": "على هذه الشبكة", "on-this-network": "على هذه الشبكة",
"routed": "توجيهّا من خلال الخادم", "routed": "توجيهّا من خلال الخادم",
"discovery": "يمكنك اكتشافك:", "discovery": "يمكنك اكتشاف:",
"on-this-network_title": "يمكن للجميع اكتشافك على هذه الشبكة.", "on-this-network_title": "يمكن للجميع اكتشافك على هذه الشبكة.",
"known-as": "‌أنت معروف بأنك:" "known-as": "‌أنت معروف بأنك:"
}, },
@@ -20,72 +20,71 @@
"message-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح", "message-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"rate-limit-join-key": "تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.", "rate-limit-join-key": "تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.",
"connecting": "يتصل …", "connecting": "يتصل …",
"pairing-key-invalidated": "المفتاح {{key}} خاطئ", "pairing-key-invalidated": "المفتاح {{key}} خاطئ.",
"pairing-key-invalid": "مُفتاح خاطئ", "pairing-key-invalid": "مُفتاح خاطئ",
"connected": "متصل", "connected": "متصل.",
"pairing-not-persistent": "الأجهزة المقترنة ليست ثابتة", "pairing-not-persistent": "الأجهزة المقترنة ليست ثابتة.",
"text-content-incorrect": "محتوى النص غير صحيح", "text-content-incorrect": "محتوى النص غير صحيح.",
"message-transfer-completed": "اكتمل نقل الرسالة", "message-transfer-completed": "اكتمل نقل الرسالة.",
"file-transfer-completed": "اكتمل نقل الملف", "file-transfer-completed": "اكتمل نقل الملف.",
"file-content-incorrect": "محتوى الملف غير صحيح", "file-content-incorrect": "محتوى الملف غير صحيح.",
"files-incorrect": "الملفات غير صحيحة", "files-incorrect": "الملفات غير صحيحة.",
"selected-peer-left": "مُحَدد الاجهزة المقترنة", "selected-peer-left": "مُحَدد الاجهزة المقترنة.",
"link-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح", "link-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"online": "لقد عدت متصلاً بالإنترنت", "online": "لقد عدت متصلاً بالإنترنت",
"public-room-left": "الخروج من الغرفة العامة {{publicRoomId}}", "public-room-left": "الخروج من الغرفة العامة {{publicRoomId}}",
"copied-text": "نُسِخَ النص إلى الحافظة", "copied-text": "نُسِخَ النص إلى الحافظة",
"display-name-random-again": "يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى", "display-name-random-again": "يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى.",
"display-name-changed-permanently": "يتم تغيير اسم العرض بشكل دائم", "display-name-changed-permanently": "يتم تغيير اسم العرض بشكل دائم.",
"copied-to-clipboard-error": "النسخ غير ممكن. انسخ يدويًا.", "copied-to-clipboard-error": "النسخ غير ممكن. انسخ يدويًا.",
"pairing-success": "الأجهزة المقترنة", "pairing-success": "الأجهزة المقترنة.",
"clipboard-content-incorrect": "محتوى الحافظة غير صحيح", "clipboard-content-incorrect": "محتوى الحافظة غير صحيح.",
"display-name-changed-temporarily": "تم تغيير اسم العرض لهذه الجلسة فقط", "display-name-changed-temporarily": "تم تغيير اسم العرض لهذه الجلسة فقط.",
"copied-to-clipboard": "تم النسخ إلى الحافظة", "copied-to-clipboard": "تم النسخ إلى الحافظة",
"offline": "انت غير متصل", "offline": "انت غير متصل",
"pairing-tabs-error": "من المستحيل إقران علامتي تبويب متصفح الويب", "pairing-tabs-error": "من المستحيل إقران علامتي تبويب متصفح الويب.",
"public-room-id-invalid": "معرف الغرفة غير صالح", "public-room-id-invalid": "معرف الغرفة غير صالح",
"click-to-download": "إضغط للتحميل", "click-to-download": "إضغط للتحميل",
"pairing-cleared": "جميع الأجهزة غير مقترنة", "pairing-cleared": "جميع الأجهزة غير مقترنة.",
"notifications-enabled": "تم تمكين الإشعارات", "notifications-enabled": "تم تمكين الإشعارات.",
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة", "online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة.",
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة", "ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة", "online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة.",
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!", "copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
"download-successful": "تم تحميل {{descriptor}}", "download-successful": "تم تحميل {{descriptor}}",
"click-to-show": "اضغط للعرض" "click-to-show": "اضغط للعرض"
}, },
"header": { "header": {
"cancel-share-mode": "تمّ", "cancel-paste-mode": "تمّ",
"theme-auto_title": غيير المظهر تلقائيا من النظام", "theme-auto_title": كيٌف المظهر مع النظام",
"install_title": "تثبيت PairDrop", "install_title": "تثبيت PairDrop",
"theme-dark_title": "إستخدم دائما المظهر المظلم", "theme-dark_title": "إستخدام دائما المظهر المظلم",
"pair-device_title": "قم بإقران أجهزتك بشكل دائم", "pair-device_title": "قم بإقران أجهزتك بشكل دائم",
"join-public-room_title": "انضم إلى الغرفة العامة مؤقتًا", "join-public-room_title": "انضم إلى الغرفة العامة مؤقتًا",
"notification_title": فعيل الإشعارات", "notification_title": شغيل الإشعارات",
"edit-paired-devices_title": "تعديل الأجهزة المقترنة", "edit-paired-devices_title": "تعديل الأجهزة المقترنة",
"language-selector_title": "إختر اللغة", "language-selector_title": "إختر اللغةعربي",
"about_title": "حول PairDrop", "about_title": "حول PairDrop",
"about_aria-label": "افتح حول PairDrop", "about_aria-label": "افتح حول PairDrop",
"theme-light_title": "إستخدم دائماً المظهر الفاتح", "theme-light_title": "إستخدم دائماً المظهر الفاتح"
"edit-share-mode": "ت٧ارف"
}, },
"instructions": { "instructions": {
"x-instructions_mobile": "انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة", "x-instructions_mobile": "انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة",
"x-instructions-share-mode_desktop": "انقر للإرسال {{descriptor}}", "click-to-send": "انقر للإرسال",
"activate-share-mode-and-other-files-plural": "و{{count}} ملفات أخرى", "activate-paste-mode-and-other-files": "و{{count}} ملفات أخرى",
"x-instructions-share-mode_mobile": "انقر للإرسال {{descriptor}}", "tap-to-send": "انقر للإرسال",
"activate-share-mode-base": "افتح PairDrop على الأجهزة الأخرى للإرسال", "activate-paste-mode-base": "افتح PairDrop على الأجهزة الأخرى للإرسال",
"no-peers-subtitle": "قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من أن تكتشف على الشبكات الأخرى", "no-peers-subtitle": "قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من إكتشافها على الشبكات الأخرى",
"activate-share-mode-shared-text": "النص المشترك", "activate-paste-mode-shared-text": "النص المشترك",
"x-instructions_desktop": "انقر لإرسال الملفات أو انقر بزر الفأرة الأيمن لإرسال رسالة", "x-instructions_desktop": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"no-peers-title": "افتح PairDrop على الأجهزة الأخرى لإرسال الملفات", "no-peers-title": "افتح PairDrop على الأجهزة الأخرى لإرسال الملفات",
"x-instructions_data-drop-bg": "حرر لتحديد المستلم", "x-instructions_data-drop-bg": "حرر لتحديد المستلم",
"no-peers_data-drop-bg": "حرر لتحديد المستلم", "no-peers_data-drop-bg": "حرر لتحديد المستلم",
"x-instructions_data-drop-peer": "قم بالتحرير لإرسالها إلى القرين" "x-instructions_data-drop-peer": "قم بالتحرير لإرسالها إلى النظير"
}, },
"peer-ui": { "peer-ui": {
"processing": "مُعالجة …", "processing": "مُعالجة …",
"click-to-send-share-mode": "انقر للإرسال {{descriptor}}", "click-to-send-paste-mode": "انقر للإرسال {{descriptor}}",
"click-to-send": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة", "click-to-send": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"waiting": "يُرجى الإنتظار…", "waiting": "يُرجى الإنتظار…",
"connection-hash": "للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين", "connection-hash": "للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين",
@@ -137,7 +136,7 @@
"auto-accept": "قبول تلقائي", "auto-accept": "قبول تلقائي",
"title-file-plural": "ملفات", "title-file-plural": "ملفات",
"send-message-title": "إرسال رسالة", "send-message-title": "إرسال رسالة",
"input-room-id-on-another-device": "‌أدخل معرف الغرفة هذا على جهاز آخر", "input-room-id-on-another-device": "‌أدخل معرف الغرفة هذا على جهاز آخر ما ",
"file-other-description-image-plural": "و{{count}} صور أخرى", "file-other-description-image-plural": "و{{count}} صور أخرى",
"enter-room-id-from-another-device": "أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة." "enter-room-id-from-another-device": "أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة."
}, },
-184
View File
@@ -1,184 +0,0 @@
{
"header": {
"about_aria-label": "Адкрыйце Аб PairDrop",
"about_title": "Аб PairDrop",
"theme-auto_title": "Аўтаматычная адаптацыя тэмы да сістэмы",
"theme-light_title": "Заўсёды выкарыстоўваць светлую тэму",
"theme-dark_title": "Заўсёды выкарыстоўваць цёмную тэму",
"notification_title": "Уключыць апавяшчэнні",
"edit-paired-devices_title": "Рэдагаваць злучаныя прылады",
"join-public-room_title": "Часова далучыцца да публічнага пакоя",
"cancel-share-mode": "Адмяніць",
"language-selector_title": "Задаць мову",
"install_title": "Усталяваць PairDrop",
"pair-device_title": "Злучыце свае прылады назаўжды",
"edit-share-mode": "Рэдагаваць",
"expand_title": "Разгарнуць радок кнопак"
},
"instructions": {
"no-peers_data-drop-bg": "Адпусціце, каб выбраць атрымальніка",
"no-peers-title": "Адкрыйце PairDrop на іншых прыладах, каб адправіць файлы",
"x-instructions_data-drop-peer": "Адпусціце, каб адправіць вузлу",
"x-instructions_data-drop-bg": "Адпусціце, каб выбраць атрымальніка",
"x-instructions-share-mode_mobile": "Дакраніцеся, каб адправіць {{descriptor}}",
"activate-share-mode-and-other-file": "і 1 іньшы файл",
"activate-share-mode-and-other-files-plural": "і {{count}} іньшых файла(ў)",
"activate-share-mode-shared-text": "агульны тэкст",
"activate-share-mode-shared-files-plural": "{{count}} агульных файлаў",
"webrtc-requirement": "Каб выкарыстоўваць гэты асобнік Pair Drop, WebRTC павінен быць уключаны!",
"no-peers-subtitle": "Злучыце прылады або ўвайдзіце ў публічны пакой, каб вас маглі выявіць з іншых сетак",
"x-instructions_mobile": "Дакраніцеся, каб адправіць файлы, або доўга трымайце, каб адправіць паведамленне",
"x-instructions-share-mode_desktop": "Націсніце, каб адправіць {{descriptor}}",
"x-instructions_desktop": "Націсніце, каб адправіць файлы, або націсніце правай кнопкай мышы, каб адправіць паведамленне",
"activate-share-mode-base": "Адкрыйце PairDrop на іншых прыладах, каб адправіць",
"activate-share-mode-shared-file": "агульны файл"
},
"footer": {
"known-as": "Вы вядомыя як:",
"display-name_data-placeholder": "Загрузка…",
"discovery": "Вас могуць выявіць:",
"on-this-network_title": "Вас можа знайсці кожны ў гэтай сетцы.",
"paired-devices": "з дапамогай злучаных прылад",
"public-room-devices_title": "Вас могуць выявіць прылады ў гэтай публічнай пакоі незалежна ад сеткі.",
"traffic": "Рух",
"display-name_title": "Зменіце назву сваёй прылады назаўжды",
"on-this-network": "у гэтай сетцы",
"paired-devices_title": "Злучаныя прылады заўсёды могуць вас выявіць незалежна ад сеткі.",
"public-room-devices": "у пакоі {{roomId}}",
"webrtc": ", калі WebRTC недаступны.",
"routed": "накіроўваецца праз сервер"
},
"dialogs": {
"hr-or": "АБО",
"cancel": "Адмяніць",
"pair": "Злучыць",
"unpair": "Разлучыць",
"paired-devices-wrapper_data-empty": "Няма злучаных прылад.",
"auto-accept": "аўтаматычнае прыняцце",
"close": "Закрыць",
"join": "Далучыцца",
"leave": "Пакінуць",
"decline": "Адмовіць",
"share": "Падзяліцца",
"copy": "Капіяваць",
"title-image": "Малюнак",
"system-language": "Мова сістэмы",
"public-room-qr-code_title": "Націсніце, каб скапіяваць спасылку на публічны пакой",
"title-file": "Файл",
"title-file-plural": "Файлы",
"message_placeholder": "Тэкст",
"language-selector-title": "Задаць мову",
"send-message-title": "Адправіць паведамленне",
"scan-qr-code": "або сканаваць QR-код.",
"enter-room-id-from-another-device": "Увядзіце ID пакоя з іншай прылады, каб далучыцца да пакоя.",
"edit-paired-devices-title": "Рэдагаваць злучаныя прылады",
"auto-accept-instructions-1": "Актываваць",
"auto-accept-instructions-2": ", каб аўтаматычна прымаць усе файлы, адпраўленыя з гэтай прылады.",
"accept": "Прыняць",
"download": "Спампаваць",
"download-again": "Спампаваць яшчэ раз",
"pair-devices-qr-code_title": "Націсніце, каб скапіраваць спасылку для спалучэння гэтай прылады",
"approve": "сцвердзіць",
"pair-devices-title": "Пастаяннае злучэнне прылад",
"enter-key-from-another-device": "Увядзіце тут ключ з іншай прылады.",
"temporary-public-room-title": "Часовы публічны пакой",
"input-room-id-on-another-device": "Увядзіце гэты ID пакоя на іншай прыладзе",
"paired-device-removed": "Злучаная прылада была выдалена.",
"would-like-to-share": "хацеў бы падзяліцца",
"send-message-to": "Каму:",
"message_title": "Устаўце паведамленне для адпраўкі",
"has-sent": "адправіў:",
"base64-processing": "Апрацоўка…",
"send": "Адправіць",
"base64-title-text": "Падзяліцца тэкстам",
"base64-title-files": "Падзяліцца файламі",
"base64-tap-to-paste": "Дакраніцеся тут, каб падзяліцца {{type}}",
"file-other-description-image": "і 1 іньшы малюнак",
"file-other-description-image-plural": "і {{count}} іншых малюнкаў",
"file-other-description-file-plural": "і {{count}} іншых файлаў",
"title-image-plural": "Малюнкі",
"share-text-subtitle": "Рэдагаваць паведамленне перад адпраўкай:",
"share-text-title": "Падзяліцца тэкставым паведамленнем",
"close-toast_title": "Закрыць апавяшчэнне",
"receive-text-title": "Паведамленне атрымана",
"input-key-on-this-device": "Увядзіце гэты ключ на іншай прыладзе",
"base64-files": "файлы",
"base64-text": "тэкст",
"base64-paste-to-send": "Устаўце сюды буфер абмену, каб падзяліцца {{type}}",
"file-other-description-file": "і 1 іньшы файл",
"receive-title": "{{descriptor}} атрымана",
"share-text-checkbox": "Заўсёды паказваць гэта дыялогавае акно пры абагульванні тэксту"
},
"about": {
"buy-me-a-coffee_title": "Купіць мне кавы!",
"mastodon_title": "Напішыце пра PairDrop на Mastodon",
"tweet_title": "Твіт пра PairDrop",
"github_title": "PairDrop на GitHub",
"custom_title": "Сачыце за намі",
"bluesky_title": "Сачыце за намі на BlueSky",
"faq_title": "Часта задаюць пытанні",
"close-about_aria-label": "Закрыць Аб PairDrop",
"claim": "Самы просты спосаб перадачы файлаў паміж прыладамі",
"privacypolicy_title": "Адкрыйце нашу палітыку прыватнасці"
},
"notifications": {
"link-received": "Спасылка атрымана {{name}} - Націсніце, каб адкрыць",
"message-received": "Паведамленне атрымана {{name}} - Націсніце, каб скапіяваць",
"click-to-download": "Націсніце, каб спампаваць",
"click-to-show": "Націсніце, каб паказаць",
"copied-text-error": "Памылка запісу ў буфер абмену. Скапіруйце ўручную!",
"online": "Вы зноў у сетцы",
"online-requirement-public-room": "Вы павінны быць падлучаныя да сеткі, каб стварыць агульны пакой",
"connecting": "Падключэнне…",
"public-room-id-invalid": "Несапраўдны ID пакоя",
"notifications-permissions-error": "Дазвол на апавяшчэнні быў заблакіраваны, бо карыстальнік некалькі разоў адхіляў запыт на дазвол. Гэта можна скінуць у меню \"Аб старонцы\", доступ да якой можна атрымаць, націснуўшы на значок замка побач з радком URL.",
"request-title": "{{name}} хоча перадаць {{count}} {{descriptor}}",
"copied-text": "Тэкст скапіраваны ў буфер абмену",
"offline": "Вы па-за сеткай",
"connected": "Падключана",
"online-requirement-pairing": "Вы павінны быць падлучаныя да сеткі для спалучэння прылад",
"pairing-key-invalidated": "Ключ {{key}} несапраўдны",
"display-name-random-again": "Адлюстраванае імя зноў згенеравалася выпадковым чынам",
"download-successful": "{{descriptor}} спампавана",
"pairing-tabs-error": "Злучэнне дзвюх укладак вэб-браўзера немагчыма",
"display-name-changed-permanently": "Адлюстроўванае імя было зменена назаўжды",
"pairing-cleared": "Усе прылады раз'яднаны",
"room-url-copied-to-clipboard": "Спасылка на публічны пакой скапіравана ў буфер абмену",
"public-room-left": "Пакінуць публічны пакой {{publicRoomId}}",
"text-content-incorrect": "Змест тэксту няправільны",
"clipboard-content-incorrect": "Змест буфера абмену няправільны",
"notifications-enabled": "Апавяшчэнні ўключаны",
"files-incorrect": "Няправільныя файлы",
"file-transfer-completed": "Перадача файла завершана",
"selected-peer-left": "Выбраны вузел выйшаў",
"copied-to-clipboard": "Скапіравана ў буфер абмену",
"pair-url-copied-to-clipboard": "Спасылка для злучэння гэтай прылады скапіравана ў буфер абмену",
"pairing-success": "Злучаныя прылады",
"copied-to-clipboard-error": "Капіраванне немагчымае. Скапіруйце ўручную.",
"file-content-incorrect": "Змест файла няправільны",
"pairing-not-persistent": "Злучаныя прылады не з'яўляюцца пастаяннымі",
"pairing-key-invalid": "Несапраўдны ключ",
"display-name-changed-temporarily": "Адлюстраванае імя зменена толькі для гэтага сеансу",
"ios-memory-limit": "Адначасовая адпраўка файлаў на iOS магчымая толькі да 200 МБ",
"message-transfer-completed": "Перадача паведамлення завершана",
"unfinished-transfers-warning": "Ёсць незавершаныя перадачы. Вы ўпэўнены, што хочаце закрыць PairDrop?",
"rate-limit-join-key": "Ліміт хуткасці дасягнуты. Пачакайце 10 секунд і паўтарыце спробу."
},
"peer-ui": {
"preparing": "Падрыхтоўка…",
"waiting": "Чаканне…",
"transferring": "Перадача…",
"processing": "Апрацоўка…",
"click-to-send-share-mode": "Націсніце, каб адправіць {{descriptor}}",
"connection-hash": "Каб праверыць бяспеку скразнога шыфравання, параўнайце гэты нумар бяспекі на абедзвюх прыладах",
"click-to-send": "Націсніце, каб адправіць файлы, або націсніце правай кнопкай мышы, каб адправіць паведамленне"
},
"document-titles": {
"file-received": "Файл атрыманы",
"image-transfer-requested": "Запытана перадача малюнкаў",
"message-received": "Паведамленне атрымана",
"message-received-plural": "Атрымана {{count}} паведамленняў",
"file-received-plural": "Атрымана {{count}} файлаў",
"file-transfer-requested": "Запытана перадача файла"
}
}
-184
View File
@@ -1,184 +0,0 @@
{
"instructions": {
"x-instructions_mobile": "Toca per enviar fitxers o mantén premut per enviar un missatge",
"no-peers-subtitle": "Vincula dispositius o entra a una sala pública per ser detectable en altres xarxes",
"x-instructions_desktop": "Fes click per enviar fitxers o click dret per enviar un missatge de text",
"no-peers-title": "Obre PairDrop a altres dispositius per enviar arxius",
"x-instructions_data-drop-peer": "Allibera per enviar a un company",
"x-instructions_data-drop-bg": "Allibera per seleccionar recipient",
"no-peers_data-drop-bg": "Allibera per seleccionar destinatari",
"activate-share-mode-base": "Obre PairDrop a altres dispositius per enviar",
"activate-share-mode-shared-files-plural": "{{count}} fitxers compartits",
"x-instructions-share-mode_desktop": "Clica per enviar {{descriptor}}",
"activate-share-mode-shared-file": "fitxer compartit",
"activate-share-mode-and-other-file": "i 1 altre fitxer",
"x-instructions-share-mode_mobile": "Toca per enviar {{descriptor}}",
"activate-share-mode-and-other-files-plural": "i {{count}} fitxers més",
"activate-share-mode-shared-text": "text compartit",
"webrtc-requirement": "Per utilitzar aquesta instància de PairDrop cal habilitar WebRTC!"
},
"header": {
"theme-auto_title": "Adapta el tema al del sistema automàticament",
"install_title": "Instal·la PairDrop",
"theme-dark_title": "Utilitza sempre el mode fosc",
"pair-device_title": "Vincula els teus dispositius permanentment",
"join-public-room_title": "Uneix-te temporalment a una sala pública",
"notification_title": "Permet les notificacions",
"edit-paired-devices_title": "Edita els dispositius vinculats",
"edit-share-mode": "Editar",
"language-selector_title": "Configurar idioma",
"cancel-share-mode": "Cancel·lar",
"about_title": "Sobre PairDrop",
"about_aria-label": "Obre Sobre PairDrop",
"theme-light_title": "Utilitza sempre el mode clar",
"expand_title": "Expandeix la fila de botons de la capçalera"
},
"dialogs": {
"message_placeholder": "Text",
"base64-paste-to-send": "Enganxa el contingut del porta-retalls aquí per compartir {{type}}",
"auto-accept-instructions-2": "per acceptar automàticament tots els fitxers enviats des d'aquell dispositiu.",
"receive-text-title": "Missatge Rebut",
"edit-paired-devices-title": "Edita els Dispositius Vinculats",
"cancel": "Cancel·lar",
"auto-accept-instructions-1": "Activar",
"pair-devices-title": "Vincula Dispositius Permanentment",
"download": "Descarregar",
"title-file": "Fitxer",
"close-toast_title": "Tanca notificació",
"base64-processing": "Processant…",
"decline": "Rebutjar",
"receive-title": "{{descriptor}} Rebut",
"share-text-checkbox": "Mostra sempre aquesta finestra de diàleg en compartir text",
"leave": "Marxar",
"message_title": "Introdueix el missatge a enviar",
"join": "Unir-se",
"title-image-plural": "Imatges",
"send": "Enviar",
"base64-title-files": "Compartir Fitxers",
"base64-tap-to-paste": "Toca aquí per compartir {{type}}",
"base64-text": "text",
"copy": "Copiar",
"file-other-description-image": "i 1 altra imatge",
"pair-devices-qr-code_title": "Clica per copiar l'enllaç per vincular aquest dispositiu",
"approve": "aprovar",
"temporary-public-room-title": "Sala Pública Temporal",
"base64-files": "fitxers",
"paired-device-removed": "Dispositiu vinculat eliminat.",
"has-sent": "ha enviat:",
"share-text-title": "Compartir Missatge de Text",
"file-other-description-file": "i 1 altre fitxer",
"public-room-qr-code_title": "Clica per copiar l'enllaç a la sala pública",
"close": "Tancar",
"system-language": "Idioma del Sistema",
"share-text-subtitle": "Edita el missatge abans d'enviar:",
"unpair": "Desvincular",
"title-image": "Imatge",
"file-other-description-file-plural": "i {{count}} altres fitxers",
"would-like-to-share": "voldria compartir",
"base64-title-text": "Compartir Text",
"send-message-to": "Per a:",
"language-selector-title": "Establir idioma",
"pair": "Vincular",
"hr-or": "O",
"scan-qr-code": "o escaneja el codi QR.",
"input-key-on-this-device": "Introdueix aquesta clau a un altre dispositiu",
"download-again": "Descarregar de nou",
"accept": "Acceptar",
"paired-devices-wrapper_data-empty": "No hi ha dispositius vinculats.",
"enter-key-from-another-device": "Introdueix la clau d'un altre dispositiu aquí.",
"share": "Compartir",
"auto-accept": "auto-acceptar",
"title-file-plural": "Fitxers",
"send-message-title": "Enviar Missatge",
"input-room-id-on-another-device": "Introdueix aquest ID de sala a un altre dispositiu",
"file-other-description-image-plural": "i {{count}} altres imatges",
"enter-room-id-from-another-device": "Introdueix l'ID de sala d'un altre dispositiu per unir-t'hi."
},
"footer": {
"webrtc": "si WebRTC no està disponible.",
"public-room-devices_title": "Pots ser descobert per dispositius en aquesta sala pública independentment de la xarxa.",
"display-name_data-placeholder": "Carregant…",
"display-name_title": "Edita el nom del teu dispositiu permanentment",
"traffic": "El trànsit és",
"paired-devices_title": "Pots ser descobert per dispositius emparellats en qualsevol moment, independentment de la xarxa.",
"public-room-devices": "a la sala {{roomId}}",
"paired-devices": "per dispositius vinculats",
"on-this-network": "En aquesta xarxa",
"routed": "encaminat a través del servidor",
"discovery": "Pots ser descobert:",
"on-this-network_title": "Pots ser descobert per qualsevol usuari en aquesta xarxa.",
"known-as": "Ets conegut com a:"
},
"notifications": {
"request-title": "{{name}} voldria transferir {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Hi ha transferències pendents. Estàs segur que vols tancar PairDrop?",
"message-received": "Missatge rebut per {{name}} - Fes clic per copiar",
"notifications-permissions-error": "El permís per notificacions ha estat bloquejat, ja que l'usuari ha refusat la sol·licitud diverses vegades. Això es pot restablir a Informació de Pàgina, a on es pot accedir clicant la icona amb el cadenat que hi ha al costat de la barra de l'URL.",
"rate-limit-join-key": "S'ha arribat al límit de ràtio. Espera 10 segons i torna-ho a intentar.",
"pair-url-copied-to-clipboard": "Enllaç per vincular aquest dispositiu copiat al porta-retalls",
"connecting": "Connectant…",
"pairing-key-invalidated": "Clau {{key}} invalidada",
"pairing-key-invalid": "Clau no vàlida",
"connected": "Connectat",
"pairing-not-persistent": "Els dispositius vinculats no són persistents",
"text-content-incorrect": "El contingut del text és incorrecte",
"message-transfer-completed": "Transferència de missatge completada",
"file-transfer-completed": "Transferència de fitxers completada",
"file-content-incorrect": "El contingut del fitxer és incorrecte",
"files-incorrect": "Els fitxers són incorrectes",
"selected-peer-left": "L'usuari seleccionat ha marxat",
"link-received": "Enllaç rebut per {{name}} - Fes clic per obrir",
"online": "Tornes a estar en línia",
"public-room-left": "Has sortit de la sala pública {{publicRoomId}}",
"copied-text": "Text copiat al porta-retalls",
"display-name-random-again": "El nom d'usuari ha estat generat novament generat aleatòriament",
"display-name-changed-permanently": "El nom d'usuari està canviat permanentment",
"copied-to-clipboard-error": "Còpia impossible. Copiar manualment.",
"pairing-success": "Dispositius vinculats",
"clipboard-content-incorrect": "El contingut del porta-retalls és incorrecte",
"display-name-changed-temporarily": "El nom d'usuari està canviat només per aquesta sessió",
"copied-to-clipboard": "Copiat al porta-retalls",
"offline": "No estàs en línia",
"pairing-tabs-error": "No es poden vincular dues pestanyes d'un navegador",
"public-room-id-invalid": "ID de sala no vàlid",
"click-to-download": "Clica per descarregar",
"pairing-cleared": "Tots els dispositius desvinculats",
"notifications-enabled": "Notificacions habilitades",
"online-requirement-pairing": "Has d'estar en línia per vincular dispositius",
"ios-memory-limit": "Tan sols és possible enviar fitxers de fins a 200 MB a iOS",
"online-requirement-public-room": "Cal que estiguis en línia per poder crear una sala pública",
"room-url-copied-to-clipboard": "Enllaç a la sala pública copiat al porta-retalls",
"copied-text-error": "L'escriptura al porta-retalls ha fallat. Copiar manualment!",
"download-successful": "{{descriptor}} descarregat",
"click-to-show": "Clica per mostrar"
},
"peer-ui": {
"processing": "Processant…",
"click-to-send-share-mode": "Clica per enviar {{descriptor}}",
"click-to-send": "Fes clic per enviar fitxers o fes clic dret per enviar un missatge",
"waiting": "Esperant…",
"connection-hash": "Per verificar la seguretat del xifratge de punta a punta, compara aquest número de seguretat en ambdós dispositius",
"preparing": "Preparant…",
"transferring": "Transferint…"
},
"about": {
"claim": "La manera més fàcil de compartir fitxers entre dispositius",
"tweet_title": "Tuiteja sobre PairDrop",
"close-about_aria-label": "Tanca Sobre PairDrop",
"buy-me-a-coffee_title": "Convida'm a un cafè!",
"github_title": "PairDrop a GitHub",
"faq_title": "Preguntes freqüents",
"mastodon_title": "Escriu sobre PairDrop a Mastodon",
"bluesky_title": "Segueix-nos a BlueSky",
"custom_title": "Segueix-nos",
"privacypolicy_title": "Obre la nostra política de privacitat"
},
"document-titles": {
"file-transfer-requested": "Transferència de Fitxers Sol·licitada",
"image-transfer-requested": "Transferència d'Imatges Sol·licitada",
"message-received-plural": "{{count}} Missatges Rebuts",
"message-received": "Missatge Rebut",
"file-received": "Fitxer Rebut",
"file-received-plural": "{{count}} Fitxers Rebuts"
}
}
-184
View File
@@ -1,184 +0,0 @@
{
"header": {
"about_aria-label": "Otevřít o PairDrop",
"about_title": "O službě PairDrop",
"language-selector_title": "Nastavit jazyk",
"theme-auto_title": "Automatické přizpůsobení tématu systému",
"pair-device_title": "Spárovat zařízení permanentně",
"theme-light_title": "Vždy používat světlé téma",
"theme-dark_title": "Vždy používat tmavé téma",
"notification_title": "Povolit upozornění",
"install_title": "Nainstalovat PairDrop",
"edit-paired-devices_title": "Upravit spárovaná zařízení",
"join-public-room_title": "Připojte se dočasně k veřejné místnosti",
"cancel-share-mode": "Zrušit",
"edit-share-mode": "Upravit",
"expand_title": "Rozbalit řádek tlačítka záhlaví"
},
"about": {
"buy-me-a-coffee_title": "Kupte mi kávu!",
"close-about_aria-label": "Zavřít O PairDrop",
"claim": "Nejjednodušší způsob přenosu souborů mezi zařízeními",
"github_title": "PairDrop na GitHubu",
"tweet_title": "Tweet o PairDrop",
"mastodon_title": "Napište o PairDrop na Mastodon",
"custom_title": "Sledujte nás",
"privacypolicy_title": "Otevřete naše zásady ochrany osobních údajů",
"bluesky_title": "Sledujte nás na BlueSky",
"faq_title": "Často kladené otázky"
},
"footer": {
"webrtc": "pokud WebRTC není k dispozici.",
"known-as": "Jste známí jako:",
"display-name_data-placeholder": "Načítání…",
"display-name_title": "Trvale upravit název zařízení",
"discovery": "Můžete být objeveni:",
"on-this-network": "na této síti",
"on-this-network_title": "V této síti vás může objevit každý.",
"paired-devices": "pomocí spárovaných zařízení",
"paired-devices_title": "Spárovaná zařízení vás mohou kdykoli objevit nezávisle na síti.",
"public-room-devices": "v místnosti {{roomId}}",
"public-room-devices_title": "Zařízení v této veřejné místnosti vás mohou objevit nezávisle na síti.",
"traffic": "Provoz je",
"routed": "směrovány přes server"
},
"dialogs": {
"auto-accept": "auto-accept",
"pair-devices-title": "Spárujte zařízení trvale",
"input-key-on-this-device": "Zadejte tento klíč na jiném zařízení",
"scan-qr-code": "nebo naskenujte QR kód.",
"enter-key-from-another-device": "Zde zadejte klíč z jiného zařízení.",
"temporary-public-room-title": "Dočasná veřejná místnost",
"input-room-id-on-another-device": "Zadejte toto ID místnosti na jiném zařízení",
"enter-room-id-from-another-device": "Chcete-li se připojit k místnosti, zadejte ID místnosti z jiného zařízení.",
"hr-or": "NEBO",
"pair": "Párovat",
"cancel": "Zrušit",
"edit-paired-devices-title": "Upravit spárovaná zařízení",
"unpair": "Zrušit spárování",
"paired-device-removed": "Spárované zařízení bylo odstraněno.",
"paired-devices-wrapper_data-empty": "Žádná spárovaná zařízení.",
"auto-accept-instructions-1": "Aktivovat",
"auto-accept-instructions-2": "automaticky přijímat všechny soubory odeslané z tohoto zařízení.",
"close": "Zavřít",
"join": "Připojit",
"leave": "Odejít",
"accept": "Přijmout",
"decline": "Odmítnout",
"would-like-to-share": "by se rád podělil",
"has-sent": "odeslal:",
"share": "Sdílet",
"download": "Stáhnout",
"send-message-title": "Poslat zprávu",
"send-message-to": "Komu:",
"message_title": "Vložte zprávu k odeslání",
"message_placeholder": "Text",
"send": "Odeslat",
"receive-text-title": "Zpráva přijata",
"copy": "Kopírovat",
"base64-title-files": "Sdílet soubory",
"base64-title-text": "Sdílet text",
"base64-processing": "Zpracovává se…",
"base64-tap-to-paste": "Klepnutím sem sdílejte {{type}}",
"base64-files": "soubory",
"file-other-description-image": "a 1 další obrázek",
"base64-paste-to-send": "Sem vložte schránku pro sdílení {{type}}",
"base64-text": "text",
"file-other-description-file": "a 1 další soubor",
"file-other-description-image-plural": "a další obrázky ({{count}})",
"file-other-description-file-plural": "a {{count}} dalších souborů",
"title-image": "Obrázek",
"title-file": "Soubor",
"title-image-plural": "Obrázky",
"title-file-plural": "Soubory",
"receive-title": "{{descriptor}} Přijato",
"download-again": "Stáhnout znovu",
"language-selector-title": "Nastavit jazyk",
"system-language": "Jazyk systému",
"public-room-qr-code_title": "Kliknutím zkopírujete odkaz do veřejné místnosti",
"pair-devices-qr-code_title": "Kliknutím zkopírujete odkaz pro spárování tohoto zařízení",
"approve": "schválit",
"share-text-title": "Sdílet textovou zprávu",
"share-text-subtitle": "Upravit zprávu před odesláním:",
"share-text-checkbox": "Při sdílení textu vždy zobrazit tento dialog",
"close-toast_title": "Zavřít oznámení"
},
"instructions": {
"no-peers_data-drop-bg": "Uvolněním vyberte příjemce",
"no-peers-title": "Otevřete PairDrop na jiných zařízeních a posílejte soubory",
"no-peers-subtitle": "Spárujte zařízení nebo vstupte do veřejné místnosti, abyste byli zjistitelní v jiných sítích",
"x-instructions_desktop": "Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu",
"x-instructions_mobile": "Klepnutím odešlete soubory nebo dlouhým klepnutím odešlete zprávu",
"x-instructions_data-drop-peer": "Uvolněním odešlete",
"x-instructions_data-drop-bg": "Uvolněním vyberte příjemce",
"x-instructions-share-mode_desktop": "Kliknutím odešlete {{descriptor}}",
"x-instructions-share-mode_mobile": "Klepnutím odešlete {{descriptor}}",
"activate-share-mode-base": "Pro odeslání otevřete PairDrop na jiných zařízeních",
"activate-share-mode-and-other-file": "a 1 další soubor",
"activate-share-mode-and-other-files-plural": "a {{count}} dalších souborů",
"activate-share-mode-shared-text": "sdílený text",
"activate-share-mode-shared-file": "sdílený soubor",
"activate-share-mode-shared-files-plural": "{{count}} sdílených souborů",
"webrtc-requirement": "Chcete-li použít PairDrop, musí být povoleno WebRTC!"
},
"notifications": {
"display-name-changed-permanently": "Zobrazované jméno je trvale změněno",
"display-name-changed-temporarily": "Zobrazované jméno je změněno pouze pro tuto relaci",
"display-name-random-again": "Zobrazované jméno je opět náhodně generováno",
"download-successful": "{{descriptor}} staženo",
"pairing-tabs-error": "Spárování dvou záložek webového prohlížeče není možné",
"pairing-success": "Zařízení spárována",
"pairing-not-persistent": "Spárovaná zařízení nejsou trvalá",
"pairing-key-invalid": "Neplatný klíč",
"pairing-key-invalidated": "Klíč {{key}} byl neplatný",
"public-room-id-invalid": "Neplatné ID místnosti",
"public-room-left": "Opustit veřejnou místnost {{publicRoomId}}",
"copied-to-clipboard": "Zkopírováno do schránky",
"pair-url-copied-to-clipboard": "Odkaz pro spárování tohoto zařízení byl zkopírován do schránky",
"room-url-copied-to-clipboard": "Odkaz do veřejné místnosti zkopírován do schránky",
"pairing-cleared": "Všechna nespárovaná zařízení",
"copied-to-clipboard-error": "Kopírování není možné. Kopírovat ručně.",
"text-content-incorrect": "Textový obsah je nesprávný",
"file-content-incorrect": "Obsah souboru je nesprávný",
"clipboard-content-incorrect": "Obsah schránky je nesprávný",
"notifications-enabled": "Oznámení povolena",
"notifications-permissions-error": "Oprávnění k oznámení bylo zablokováno, protože uživatel několikrát odmítl výzvu k povolení. Toto lze resetovat v části Informace o stránce, ke které se dostanete kliknutím na ikonu zámku vedle řádku adresy URL.",
"link-received": "Odkaz obdržel {{name}} kliknutím otevřete",
"message-received": "Zpráva přijatá uživatelem {{name}} kliknutím zkopírujte",
"click-to-download": "Kliknutím stáhnete",
"request-title": "{{name}} chce přenést {{count}} {{descriptor}}",
"copied-text": "Text byl zkopírován do schránky",
"click-to-show": "Kliknutím zobrazíte",
"copied-text-error": "Zápis do schránky se nezdařil. Zkopírujte ručně!",
"offline": "Jste offline",
"online": "Jste zpět online",
"connected": "Připojeno",
"online-requirement-public-room": "Chcete-li vytvořit veřejnou místnost, musíte být online",
"online-requirement-pairing": "Chcete-li spárovat zařízení, musíte být online",
"connecting": "Připojování…",
"files-incorrect": "Soubory jsou nesprávné",
"file-transfer-completed": "Přenos souborů byl dokončen",
"message-transfer-completed": "Přenos zprávy byl dokončen",
"ios-memory-limit": "Odesílání souborů do iOS je možné pouze do velikosti 200 MB najednou",
"unfinished-transfers-warning": "Existují nedokončené přenosy Opravdu chcete zavřít PairDrop?",
"rate-limit-join-key": "Bylo dosaženo limitu. Počkejte 10 sekund a zkuste to znovu.",
"selected-peer-left": "Vybraný partner odešel"
},
"document-titles": {
"file-received": "Soubor byl přijat",
"file-received-plural": "Počet přijatých souborů: {{count}}",
"file-transfer-requested": "Požadován přenos souboru",
"message-received": "Zpráva přijata",
"image-transfer-requested": "Požadován přenos obrázku",
"message-received-plural": "Počet přijatých zpráv: {{count}}"
},
"peer-ui": {
"click-to-send-share-mode": "Kliknutím odešlete {{descriptor}}",
"click-to-send": "Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu",
"transferring": "Přenáší se…",
"connection-hash": "Chcete-li ověřit bezpečnost šifrování typu end-to-end, porovnejte toto číslo zabezpečení na obou zařízeních",
"preparing": "Připravuje se…",
"waiting": "Čekání…",
"processing": "Zpracovává se…"
}
}
-184
View File
@@ -1,184 +0,0 @@
{
"notifications": {
"public-room-left": "Forlod det offentlige rum {{publicRoomId}}",
"room-url-copied-to-clipboard": "Link til offentligt rum kopieret til udklipsholder",
"notifications-enabled": "Notifikationer aktiveret",
"notifications-permissions-error": "Notifikationstilladelsen er blevet blokeret, da brugeren har afvist tilladelsesprompten flere gange. Dette kan nulstilles i sideoplysninger, som du kan få adgang til ved at klikke på låseikonet ved siden af URL-linjen.",
"copied-text-error": "Skrivning til udklipsholder mislykkedes. Kopier manuelt!",
"ios-memory-limit": "Det er kun muligt at sende filer til iOS op til 200 MB på én gang",
"display-name-random-again": "Vist navn genereres tilfældigt igen",
"display-name-changed-permanently": "Det viste navn blec ændret permanent",
"display-name-changed-temporarily": "Vist navn blev kun ændret for denne session",
"download-successful": "{{descriptor}} hentet",
"pairing-tabs-error": "Det er umuligt at parre to webbrowserfaner",
"pairing-success": "Enheder parret",
"pairing-not-persistent": "Parrede enheder er ikke vedvarende",
"pairing-key-invalid": "Ugyldig nøgle",
"pairing-key-invalidated": "Nøglen {{key}} er ugyldig",
"pairing-cleared": "Alle enheder er frakoblet",
"public-room-id-invalid": "Ugyldigt rum-id",
"copied-to-clipboard": "Kopieret til udklipsholder",
"pair-url-copied-to-clipboard": "Link til at parre denne enhed kopieret til udklipsholder",
"copied-to-clipboard-error": "Kopiering ikke mulig. Kopier manuelt.",
"text-content-incorrect": "Tekstindholdet er forkert",
"file-content-incorrect": "Filens indhold er forkert",
"clipboard-content-incorrect": "Udklipsholderens indhold er forkert",
"link-received": "Link modtaget af {{name}} - Klik for at åbne",
"message-received": "Besked modtaget af {{name}} - Klik for at kopiere",
"click-to-download": "Klik for at hente",
"request-title": "{{name}} vil gerne overføre {{count}} {{descriptor}}",
"click-to-show": "Klik for at vise",
"copied-text": "Kopieret tekst til udklipsholder",
"offline": "Du er offline",
"online": "Du er online igen",
"connected": "Forbundet",
"online-requirement-pairing": "Du skal være online for at parre enheder",
"online-requirement-public-room": "Du skal være online for at oprette et offentligt rum",
"connecting": "Forbinder…",
"files-incorrect": "Filerne er forkerte",
"file-transfer-completed": "Filoverførsel gennemført",
"message-transfer-completed": "Beskedoverførsel gennemført",
"unfinished-transfers-warning": "Der er uafsluttede overførsler. Er du sikker på, at du vil lukke PairDrop?",
"rate-limit-join-key": "Satsgrænsen er nået. Vent 10 sekunder, og prøv igen.",
"selected-peer-left": "Valgt peer forlod"
},
"dialogs": {
"message_placeholder": "Tekst",
"base64-files": "filer",
"file-other-description-image": "og 1 andet billede",
"file-other-description-file": "og 1 anden fil",
"download-again": "Hent igen",
"system-language": "Systemsprog",
"pair-devices-qr-code_title": "Klik for at kopiere linket for at parre denne enhed",
"enter-key-from-another-device": "Indtast nøgle fra en anden enhed her.",
"temporary-public-room-title": "Midlertidigt offentligt rum",
"edit-paired-devices-title": "Rediger parrede enheder",
"auto-accept-instructions-2": "for automatisk at acceptere alle filer sendt fra den pågældende enhed.",
"pair-devices-title": "Par enheder permanent",
"input-key-on-this-device": "Indtast denne nøgle på en anden enhed",
"scan-qr-code": "eller scan QR-koden.",
"input-room-id-on-another-device": "Indtast dette rum-id på en anden enhed",
"enter-room-id-from-another-device": "Indtast rum-id fra en anden enhed for at deltage i rummet.",
"hr-or": "ELLER",
"pair": "Par",
"cancel": "Annuller",
"unpair": "Fjern parring",
"paired-device-removed": "Parret enhed er blevet fjernet.",
"paired-devices-wrapper_data-empty": "Ingen parrede enheder.",
"auto-accept-instructions-1": "Aktiver",
"auto-accept": "auto-accepter",
"close": "Luk",
"join": "Forbinde",
"leave": "Forlad",
"would-like-to-share": "gerne vil dele",
"accept": "Accepter",
"decline": "Nægt",
"has-sent": "har sendt:",
"share": "Del",
"download": "Hent",
"send-message-title": "Send besked",
"send-message-to": "Til:",
"message_title": "Indsæt besked for at sende",
"send": "Send",
"receive-text-title": "Besked modtaget",
"copy": "Kopier",
"base64-title-files": "Del filer",
"base64-title-text": "Del tekst",
"base64-processing": "Behandler…",
"base64-tap-to-paste": "Tryk her for at dele {{type}}",
"base64-paste-to-send": "Indsæt udklipsholder her for at dele {{type}}",
"base64-text": "tekst",
"file-other-description-image-plural": "og {{count}} andre billeder",
"file-other-description-file-plural": "og {{count}} andre filer",
"title-image": "Billede",
"title-file": "Fil",
"title-image-plural": "Billeder",
"title-file-plural": "Filer",
"receive-title": "{{descriptor}} Modtaget",
"language-selector-title": "Indstil sprog",
"public-room-qr-code_title": "Klik for at kopiere linket til det offentlige rum",
"approve": "godkend",
"share-text-title": "Del tekstbesked",
"share-text-subtitle": "Rediger besked, før du sender:",
"share-text-checkbox": "Vis altid denne dialogboks, når du deler tekst",
"close-toast_title": "Luk besked"
},
"about": {
"claim": "Den nemmeste måde at overføre filer på tværs af enheder",
"faq_title": "Ofte stillede spørgsmål",
"close-about_aria-label": "Luk Om PairDrop",
"github_title": "PairDrop på GitHub",
"buy-me-a-coffee_title": "Køb mig en kop kaffe!",
"tweet_title": "Tweet om PairDrop",
"mastodon_title": "Skriv om PairDrop på Mastodon",
"bluesky_title": "Følg os på BlueSky",
"custom_title": "Følg os",
"privacypolicy_title": "Åbn vores privatlivspolitik"
},
"header": {
"language-selector_title": "Indstil sprog",
"about_aria-label": "Åbn Om PairDrop",
"theme-auto_title": "Tilpas temaet til systemet automatisk",
"theme-light_title": "Brug altid lyst tema",
"theme-dark_title": "Brug altid mørkt tema",
"notification_title": "Aktiver notifikationer",
"install_title": "Installer PairDrop",
"pair-device_title": "Par dine enheder permanent",
"edit-paired-devices_title": "Rediger parrede enheder",
"join-public-room_title": "Deltag midlertidigt i det offentlige rum",
"cancel-share-mode": "Annuller",
"edit-share-mode": "Redigere",
"expand_title": "Udvid overskriftsknaprækken",
"about_title": "Om PairDrop"
},
"instructions": {
"no-peers-subtitle": "Par enheder, eller gå ind i et offentligt rum for at være synlig på andre netværk",
"x-instructions_desktop": "Klik for at sende filer eller højreklik for at sende en besked",
"activate-share-mode-base": "Åbn PairDrop på andre enheder for at sende",
"no-peers_data-drop-bg": "Slip for at vælge modtager",
"no-peers-title": "Åbn PairDrop på andre enheder for at sende filer",
"x-instructions_mobile": "Tryk for at sende filer, eller tryk længe for at sende en besked",
"x-instructions_data-drop-peer": "Slip for at sende til peer",
"x-instructions_data-drop-bg": "Slip for at vælge modtager",
"x-instructions-share-mode_desktop": "Klik for at sende {{descriptor}}",
"x-instructions-share-mode_mobile": "Tryk for at sende {{descriptor}}",
"activate-share-mode-and-other-file": "og 1 anden fil",
"activate-share-mode-and-other-files-plural": "og {{count}} andre filer",
"activate-share-mode-shared-text": "delt tekst",
"activate-share-mode-shared-file": "delt fil",
"activate-share-mode-shared-files-plural": "{{count}} delte filer",
"webrtc-requirement": "For at bruge denne PairDrop-instans skal WebRTC være aktiveret!"
},
"footer": {
"on-this-network_title": "Du kan blive opdaget af alle på dette netværk.",
"public-room-devices_title": "Du kan blive opdaget af enheder i dette offentlige rum uafhængigt af netværket.",
"known-as": "Du er kendt som:",
"display-name_data-placeholder": "Indlæser…",
"display-name_title": "Rediger dit enhedsnavn permanent",
"discovery": "Du kan blive opdaget:",
"on-this-network": "på dette netværk",
"paired-devices": "af parrede enheder",
"paired-devices_title": "Du kan til enhver tid blive opdaget af parrede enheder uafhængigt af netværket.",
"public-room-devices": "i rum {{roomId}}",
"traffic": "Trafikken er",
"routed": "dirigeret gennem serveren",
"webrtc": "hvis WebRTC ikke er tilgængelig."
},
"document-titles": {
"file-received": "Fil modtaget",
"file-received-plural": "{{count}} Filer modtaget",
"file-transfer-requested": "Filoverførsel anmodet",
"image-transfer-requested": "Billedoverførsel anmodet",
"message-received": "Besked modtaget",
"message-received-plural": "{{count}} meddelelser modtaget"
},
"peer-ui": {
"click-to-send-share-mode": "Klik for at sende {{descriptor}}",
"click-to-send": "Klik for at sende filer eller højreklik for at sende en besked",
"connection-hash": "For at kontrollere sikkerheden for end-to-end-kryptering skal du sammenligne dette sikkerhedsnummer på begge enheder",
"preparing": "Forbereder…",
"waiting": "Venter…",
"processing": "Behandler…",
"transferring": "Overfører…"
}
}
+44 -69
View File
@@ -4,20 +4,18 @@
"notification_title": "Benachrichtigungen aktivieren", "notification_title": "Benachrichtigungen aktivieren",
"about_aria-label": "Über PairDrop öffnen", "about_aria-label": "Über PairDrop öffnen",
"install_title": "PairDrop installieren", "install_title": "PairDrop installieren",
"pair-device_title": "Kopple deine Geräte dauerhaft", "pair-device_title": "Deine Geräte dauerhaft koppeln",
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten", "edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
"theme-auto_title": "Systemstil verwenden", "theme-auto_title": "Systemstil verwenden",
"theme-dark_title": "Immer dunklen Stil verwenden", "theme-dark_title": "Immer dunklen Stil verwenden",
"theme-light_title": "Immer hellen Stil verwenden", "theme-light_title": "Immer hellen Stil verwenden",
"cancel-share-mode": "Fertig", "cancel-paste-mode": "Fertig",
"language-selector_title": "Sprache Wählen", "language-selector_title": "Sprache Wählen",
"join-public-room_title": "Öffentlichen Raum temporär betreten", "join-public-room_title": "Öffentlichen Raum temporär betreten"
"edit-share-mode": "Bearbeiten",
"expand_title": "Schaltflächenzeile ausklappen"
}, },
"dialogs": { "dialogs": {
"share": "Teilen", "share": "Teilen",
"download": "Download", "download": "Herunterladen",
"pair-devices-title": "Geräte Dauerhaft Koppeln", "pair-devices-title": "Geräte Dauerhaft Koppeln",
"input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein", "input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein",
"enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.", "enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.",
@@ -36,24 +34,24 @@
"would-like-to-share": "möchte Folgendes teilen", "would-like-to-share": "möchte Folgendes teilen",
"send": "Senden", "send": "Senden",
"copy": "Kopieren", "copy": "Kopieren",
"receive-text-title": "Nachricht Erhalten", "receive-text-title": "Textnachricht Erhalten",
"file-other-description-image-plural": "und {{count}} andere Bilder", "file-other-description-image-plural": "und {{count}} andere Bilder",
"file-other-description-file-plural": "und {{count}} andere Dateien", "file-other-description-file-plural": "und {{count}} andere Dateien",
"auto-accept-instructions-1": "Aktiviere", "auto-accept-instructions-1": "Aktiviere",
"auto-accept": "automatisch-akzeptieren", "auto-accept": "auto-accept",
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.", "auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
"has-sent": "hat Folgendes gesendet:", "has-sent": "hat Folgendes gesendet:",
"send-message-title": "Nachricht Senden", "send-message-title": "Textnachricht Senden",
"send-message-to": "An:", "send-message-to": "Sende eine Textnachricht an",
"base64-tap-to-paste": "Hier tippen, um {{type}} zu teilen", "base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
"base64-paste-to-send": "Hier einfügen, um {{type}} zu teilen", "base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
"base64-text": "Text", "base64-text": "Text",
"base64-files": "Dateien", "base64-files": "Dateien",
"base64-processing": "Bearbeitung läuft…", "base64-processing": "Bearbeitung läuft…",
"file-other-description-image": "und ein anderes Bild", "file-other-description-image": "und ein anderes Bild",
"file-other-description-file": "und eine andere Datei", "file-other-description-file": "und eine andere Datei",
"receive-title": "{{descriptor}} Erhalten", "receive-title": "{{descriptor}} Erhalten",
"download-again": "Erneuter Download", "download-again": "Erneut herunterladen",
"system-language": "Systemsprache", "system-language": "Systemsprache",
"language-selector-title": "Sprache Einstellen", "language-selector-title": "Sprache Einstellen",
"hr-or": "ODER", "hr-or": "ODER",
@@ -62,19 +60,7 @@
"leave": "Verlassen", "leave": "Verlassen",
"join": "Betreten", "join": "Betreten",
"enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.", "enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.",
"temporary-public-room-title": "Temporärer Öffentlicher Raum", "temporary-public-room-title": "Temporärer Öffentlicher Raum"
"message_title": "Nachricht zum Senden hier einfügen",
"pair-devices-qr-code_title": "Klicke, um Link zum Koppeln mit diesem Gerät zu kopieren",
"public-room-qr-code_title": "Klicke, um Link zu diesem öffentlichen Raum zu kopieren",
"message_placeholder": "Text",
"close-toast_title": "Benachrichtigung schließen",
"share-text-checkbox": "Diesen Dialog immer anzeigen, wenn Text geteilt wird",
"base64-title-files": "Teile Dateien",
"approve": "bestätigen",
"paired-device-removed": "Gekoppeltes Gerät wurde entfernt.",
"share-text-title": "Teile Nachricht",
"share-text-subtitle": "Bearbeite Nachricht vor dem Senden:",
"base64-title-text": "Teile Text"
}, },
"about": { "about": {
"tweet_title": "Über PairDrop twittern", "tweet_title": "Über PairDrop twittern",
@@ -82,15 +68,11 @@
"close-about_aria-label": "Schließe Über PairDrop", "close-about_aria-label": "Schließe Über PairDrop",
"github_title": "PairDrop auf GitHub", "github_title": "PairDrop auf GitHub",
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!", "buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
"claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen", "claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen"
"bluesky_title": "Folge uns auf BlueSky",
"privacypolicy_title": "Öffne unsere Datenschutzerklärung",
"mastodon_title": "Schreibe über PairDrop auf Mastodon",
"custom_title": "Folge uns"
}, },
"footer": { "footer": {
"known-as": "Du wirst angezeigt als:", "known-as": "Du wirst angezeigt als:",
"display-name_title": "Ändere deinen Gerätenamen dauerhaft", "display-name_title": "Setze einen permanenten Gerätenamen",
"on-this-network": "in diesem Netzwerk", "on-this-network": "in diesem Netzwerk",
"paired-devices": "für gekoppelte Geräte", "paired-devices": "für gekoppelte Geräte",
"traffic": "Datenverkehr wird", "traffic": "Datenverkehr wird",
@@ -98,7 +80,7 @@
"routed": "durch den Server geleitet", "routed": "durch den Server geleitet",
"webrtc": "wenn WebRTC nicht verfügbar ist.", "webrtc": "wenn WebRTC nicht verfügbar ist.",
"display-name_data-placeholder": "Lade…", "display-name_data-placeholder": "Lade…",
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum gefunden werden, egal in welchem Netzwerk.", "public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum gefunden werden, unabhängig von deinem Netzwerk.",
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.", "paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
"public-room-devices": "in Raum {{roomId}}", "public-room-devices": "in Raum {{roomId}}",
"discovery": "Du bist sichtbar:", "discovery": "Du bist sichtbar:",
@@ -109,76 +91,69 @@
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren", "message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
"click-to-download": "Klicken zum Download", "click-to-download": "Klicken zum Download",
"copied-text": "Text in die Zwischenablage kopiert", "copied-text": "Text in die Zwischenablage kopiert",
"connected": "Verbunden", "connected": "Verbunden.",
"pairing-success": "Geräte gekoppelt", "pairing-success": "Geräte gekoppelt.",
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert", "display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden", "pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent", "pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
"pairing-key-invalid": "Ungültiger Schlüssel", "pairing-key-invalid": "Ungültiger Schlüssel",
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht", "pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
"copied-to-clipboard": "In die Zwischenablage kopiert", "copied-to-clipboard": "In die Zwischenablage kopiert",
"text-content-incorrect": "Textinhalt ist fehlerhaft", "text-content-incorrect": "Textinhalt ist fehlerhaft.",
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft", "clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!", "copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
"file-content-incorrect": "Dateiinhalt ist fehlerhaft", "file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
"notifications-enabled": "Benachrichtigungen aktiviert", "notifications-enabled": "Benachrichtigungen aktiviert.",
"offline": "Du bist offline", "offline": "Du bist offline",
"online": "Du bist wieder Online", "online": "Du bist wieder Online",
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?", "unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert", "display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
"download-successful": "{{descriptor}} heruntergeladen", "download-successful": "{{descriptor}} heruntergeladen",
"pairing-cleared": "Alle Geräte entkoppelt", "pairing-cleared": "Alle Geräte entkoppelt.",
"click-to-show": "Klicken zum Anzeigen", "click-to-show": "Klicken zum Anzeigen",
"online-requirement": "Du musst online sein um Geräte zu koppeln.", "online-requirement": "Du musst online sein um Geräte zu koppeln.",
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert", "display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert.",
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen", "request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
"connecting": "Verbindung wird hergestellt…", "connecting": "Verbindung wird hergestellt…",
"files-incorrect": "Dateien sind fehlerhaft", "files-incorrect": "Dateien sind fehlerhaft.",
"file-transfer-completed": "Dateitransfer abgeschlossen", "file-transfer-completed": "Dateiübertragung abgeschlossen.",
"message-transfer-completed": "Nachricht übertragen", "message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.", "rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
"selected-peer-left": "Ausgewählter Peer ist gegangen", "selected-peer-left": "Ausgewählter Peer ist gegangen.",
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB", "ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen", "public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.", "copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
"public-room-id-invalid": "Ungültige Raum-ID", "public-room-id-invalid": "Ungültige Raum-ID",
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln", "online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können", "online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können."
"notifications-permissions-error": "Benachrichtigungen wurden blockiert, weil der Nutzer die Berechtigungsanfrage mehrfach abgelehnt hat. Dies kann in den Einstellungen der Website zurückgesetzt werden, welche durch Klick auf das Schloss Symbol neben der URL Leiste erreicht werden können.",
"pair-url-copied-to-clipboard": "Link zum Koppeln mit diesem Gerät in Zwischenablage kopiert",
"room-url-copied-to-clipboard": "Link zu diesem öffentlichen Raum in Zwischenablage kopiert"
}, },
"instructions": { "instructions": {
"x-instructions_desktop": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Nachricht zu senden", "x-instructions_desktop": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden", "no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen", "no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
"no-peers-subtitle": "Kopple Geräte oder betritt einen öffentlichen Raum, um in anderen Netzwerken sichtbar zu sein", "no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
"x-instructions-share-mode_desktop": "Klicke zum Senden von {{descriptor}}", "click-to-send": "Klicke zum Senden von",
"x-instructions-share-mode_mobile": "Tippe zum Senden von {{descriptor}}", "tap-to-send": "Tippe zum Senden von",
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden", "x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen", "x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
"x-instructions_mobile": "Tippe, um Dateien zu senden oder tippe lange, um Nachrichten zu senden", "x-instructions_mobile": "Tippe, um Dateien zu senden oder tippe lange, um Nachrichten zu senden",
"activate-share-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von", "activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
"activate-share-mode-and-other-files-plural": "und {{count}} anderen Dateien", "activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
"activate-share-mode-shared-text": "freigegebenem Text", "activate-paste-mode-shared-text": "freigegebenem Text"
"webrtc-requirement": "Um diese PairDrop Instanz zu verwenden muss WebRTC aktiviert sein!",
"activate-share-mode-shared-files-plural": "{{count}} geteilte Dateien",
"activate-share-mode-shared-file": "geteilte Datei",
"activate-share-mode-and-other-file": "und 1 andere Datei"
}, },
"document-titles": { "document-titles": {
"file-transfer-requested": "Dateitransfer beantragt", "file-transfer-requested": "Dateiübertragung angefordert",
"file-received": "Datei erhalten", "file-received": "Datei erhalten",
"file-received-plural": "{{count}} Dateien erhalten", "file-received-plural": "{{count}} Dateien erhalten",
"message-received": "Nachricht erhalten", "message-received": "Nachricht erhalten",
"message-received-plural": "{{count}} Nachrichten erhalten", "message-received-plural": "{{count}} Nachrichten erhalten",
"image-transfer-requested": "Transfer von Bildern beantragt" "image-transfer-requested": "Bilder Transfer beantragt"
}, },
"peer-ui": { "peer-ui": {
"click-to-send": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden", "click-to-send": "Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden",
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten", "connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
"waiting": "Warte…", "waiting": "Warte…",
"click-to-send-share-mode": "Klicken um {{descriptor}} zu senden", "click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
"transferring": "Übertragung läuft…", "transferring": "Übertragung läuft…",
"processing": "Bearbeitung läuft…", "processing": "Bearbeitung läuft…",
"preparing": "Vorbereitung läuft…" "preparing": "Vorbereitung läuft…"

Some files were not shown because too many files have changed in this diff Show More