Showing preview only (855K chars total). Download the full file or copy to clipboard to get everything.
Repository: schlagmichdoch/PairDrop
Branch: master
Commit: 31ec776fb304
Files: 102
Total size: 819.4 KB
Directory structure:
gitextract_eu7hkmeg/
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── enhancement.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── docker-image.yml
│ ├── github-image.yml
│ └── zip-release.yml
├── .gitignore
├── .npmrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── dev/
│ ├── nginx/
│ │ └── default.conf
│ ├── nginx-with-openssl.Dockerfile
│ └── openssl/
│ ├── create.sh
│ ├── pairdropCA.cnf
│ └── pairdropCert.cnf
├── docker-compose-coturn.yml
├── docker-compose-dev.yml
├── docker-compose.yml
├── docs/
│ ├── docker-swarm-usage.md
│ ├── faq.md
│ ├── host-your-own.md
│ ├── how-to.md
│ └── technical-documentation.md
├── licenses/
│ ├── BSD_3-Clause-zip-js
│ ├── MIT-NoSleep
│ └── MIT-heic2any
├── package.json
├── pairdrop-cli/
│ ├── .gitignore
│ ├── .pairdrop-cli-config.example
│ ├── pairdrop
│ ├── pairdrop.sh
│ ├── send with PairDrop.lnk
│ ├── send-with-pairdrop
│ └── send-with-pairdrop.ps1
├── public/
│ ├── fonts/
│ │ └── OpenSans/
│ │ ├── OFL.txt
│ │ └── README.txt
│ ├── images/
│ │ └── snapdrop-graphics.sketch
│ ├── index.html
│ ├── lang/
│ │ ├── ar.json
│ │ ├── be.json
│ │ ├── bg.json
│ │ ├── bn.json
│ │ ├── ca.json
│ │ ├── cs.json
│ │ ├── da.json
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── et.json
│ │ ├── eu.json
│ │ ├── fa.json
│ │ ├── fi.json
│ │ ├── fr.json
│ │ ├── he.json
│ │ ├── hu.json
│ │ ├── id.json
│ │ ├── it.json
│ │ ├── ja.json
│ │ ├── kab.json
│ │ ├── kn.json
│ │ ├── ko.json
│ │ ├── nb.json
│ │ ├── nl.json
│ │ ├── nn.json
│ │ ├── pl.json
│ │ ├── pt-BR.json
│ │ ├── ro.json
│ │ ├── ru.json
│ │ ├── sk.json
│ │ ├── ta.json
│ │ ├── th.json
│ │ ├── tr.json
│ │ ├── uk.json
│ │ ├── zh-CN.json
│ │ ├── zh-HK.json
│ │ └── zh-TW.json
│ ├── manifest.json
│ ├── robots.txt
│ ├── scripts/
│ │ ├── browser-tabs-connector.js
│ │ ├── localization.js
│ │ ├── main.js
│ │ ├── network.js
│ │ ├── persistent-storage.js
│ │ ├── ui-main.js
│ │ ├── ui.js
│ │ ├── util.js
│ │ └── worker/
│ │ └── canvas-worker.js
│ ├── service-worker.js
│ ├── sounds/
│ │ └── blop.ogg
│ └── styles/
│ ├── styles-deferred.css
│ └── styles-main.css
├── rtc_config_example.json
├── server/
│ ├── helper.js
│ ├── index.js
│ ├── peer.js
│ ├── server.js
│ └── ws-server.js
└── turnserver_example.conf
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.github
.git*
.idea
dev
docs
licenses
node_modules
pairdrop-cli
*.md
*.yml
Dockerfile
rtc_config_example.json
turnserver_example.conf
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: schlagmichdoch
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://buymeacoffee.com/pairdrop
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug Report
about: Create a report to help us improve. Please check the FAQ first.
title: '[Bug] '
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes
Version: v1.11.2
**Bug occurs on self-hosted PairDrop instance**
No | Yes
**Self-Hosted Setup**
Proxy: Nginx | Apache2
Deployment: docker run | docker compose | npm run start:prod
Version: v1.11.2
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/enhancement.md
================================================
---
name: Enhancement
about: Enhancements and feature requests are always welcome. See discussions regarding central topics.
title: '[Enhancement] '
labels: 'enhancement'
assignees: ''
---
**What problem is solved by the new feature**
What's the motivation for this topic
**Describe the feature**
A clear and concise description of what the new feature/enhancement is.
**Drafts**
Screenshots of Draw.io graph or drawn sketch.
**Additional context**
Add any other context here.
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/docker-image.yml
================================================
# 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
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Build the Docker image
run: docker build --pull . -f Dockerfile -t pairdrop
================================================
FILE: .github/workflows/github-image.yml
================================================
# 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 Docker image and push it to ghcr.io whenever a new version tag is pushed
name: GHCR Image CI
on:
push:
tags:
- "v*.*.*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup qemu
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Log in to the Container registry
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@31cebacef4805868f9ce9a0cb03ee36c32df2ac4 # v5.3.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/zip-release.yml
================================================
# 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 }}
================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
/dev/certs
qrcode-svg/
turnserver.conf
rtc_config.json
ssl/
================================================
FILE: .npmrc
================================================
engine-strict=true
================================================
FILE: CONTRIBUTING.md
================================================
# Priorities
- PairDrop should be extremely simple, clean, and easy to use.
- The main user flow should never be obstructed!
- New features must be tested thoroughly before we are able to merge them.
- Stability always comes first!
# Agenda
PairDrop is a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).
# Contributing guidelines
Make sure to follow these guidelines before opening an [issue](https://github.com/schlagmichdoch/pairdrop/issues/new/choose) or a [pull request](https://github.com/schlagmichdoch/pairdrop/pulls):
- Before opening an issue of a pull request, please check if the issue or the pull request already exists.
- Pull requests for packages updates are not allowed since there is [Dependabot](https://github.com/schlagmichdoch/pairdrop/blob/master/.github/dependabot.yml) that checks them automatically.
- If you don't know how to contribute, also if you don't know JavaScript or Node.js, you can still share your awesome ideas with a new issue (feature request) and check the whole project for misspellings, too.
================================================
FILE: Dockerfile
================================================
FROM alpine:latest
WORKDIR /home/node/app
COPY package*.json ./
RUN apk add --no-cache nodejs npm
RUN NODE_ENV="production" npm ci --omit=dev
# Directories and files excluded via .dockerignore
COPY . .
# environment settings
ENV NODE_ENV="production"
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1
ENTRYPOINT ["npm", "start"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
<div align="center">
<a href="https://github.com/schlagmichdoch/PairDrop">
<img src="public/images/android-chrome-512x512.png" alt="Logo" width="150" height="150">
</a>
# _Send it_, with [PairDrop](https://pairdrop.net)
<p>
Local file sharing <a href="https://pairdrop.net"><strong>in your web browser</strong></a>.
<br>
Inspired by Apple's AirDrop.
<br>
Fork of Snapdrop.
<br>
<br>
<a href="https://github.com/schlagmichdoch/PairDrop/issues">Report a bug</a>
<br />
<a href="https://github.com/schlagmichdoch/PairDrop/issues">Request feature</a>
</p>
</div>
<br>
## Features
File sharing on your local network that works on all platforms.
- A multi-platform AirDrop-like solution that works.
- Send images, documents or text via peer-to-peer connection to devices on the same local network.
- Internet transfers
- Join temporary public rooms to transfer files easily over the Internet.
- Web-app
- Works on all devices with a modern web-browser.
Send a file from your phone to your laptop?
<br>Share photos in original quality with friends using Android and iOS?
<br>Share private files peer-to-peer between Linux systems?
<img src="docs/pairdrop_screenshot_mobile.gif" alt="Screenshot GIF showing PairDrop in use" style="width: 300px">
## Differences to the [Snapdrop](https://github.com/RobinLinus/snapdrop) it is based on
<details><summary>View all differences</summary>
### Paired Devices and Public Rooms — Internet Transfer
* Transfer files over the Internet between paired devices or by entering temporary public rooms.
* Connect to devices in complex network environments (public Wi-Fi, company network, iCloud Private Relay, VPN, etc.).
* 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 from the local network, in the same public room, or previously paired are shown.
#### Persistent Device Pairing
Always connect to known devices
* Pair devices via a 6-digit code or a QR-Code.
* Paired devices always find each other via shared secrets independently of their local network.
* Pairing is persistent. You find your devices even after reopening PairDrop.
* You can edit and unpair devices easily.
#### Temporary Public Rooms
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.
* All devices in the same public room see each other.
* Public rooms are temporary. Closing PairDrop leaves all rooms.
### [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.
* Multiple files are downloaded as a ZIP file
* Download, share or save to gallery via the "Share" menu on Android and iOS.
* 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 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 the context menu on Windows](docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from the "Share" menu on iOS](docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from the "Share" menu on Android](docs/how-to.md#send-directly-from-share-menu-on-android)
* [Send directly via the command-line interface](docs/how-to.md#send-directly-via-command-line-interface)
### Other Changes
* Change your display name to easily differentiate your devices.
* [Paste files/text and choose the recipient afterwards ](https://github.com/RobinLinus/snapdrop/pull/534)
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
* Warn user before PairDrop is closed on file transfer
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
* [Video and audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads))
* Switch theme back to auto/system after dark or light mode is on
* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))
* Auto-restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself, you can [set your own STUN/TURN servers](docs/host-your-own.md#specify-stunturn-servers)
* Translations.
</details>
## Translate PairDrop on [Hosted Weblate](https://hosted.weblate.org/engage/pairdrop/)
<a href="https://hosted.weblate.org/engage/pairdrop/">
<img src="https://hosted.weblate.org/widget/pairdrop/horizontal-blue.svg" alt="Translation status" style="width: 300px" />
</a>
## Built with the following awesome technologies:
* Vanilla HTML5 / JS ES6 / CSS 3 frontend
* [WebRTC](http://webrtc.org/) / WebSockets
* [Node.js](https://nodejs.org/en/) backend
* [Progressive web app (PWA)](https://en.wikipedia.org/wiki/Progressive_web_app) unified functionality
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) storage handling
* [zip.js](https://gildas-lormeau.github.io/zip.js/) library
* [cyrb53](https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js) super-fast hash function
* [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep))
* [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any))
* [Weblate](https://weblate.org/) web-based localization tool
* [BrowserStack](https://www.browserstack.com/) This project is tested with BrowserStack
[FAQ](docs/faq.md)
[Host your own instance with Docker or Node.js](docs/host-your-own.md).
## Support
<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;" >
</a>
<br />
<br />
PairDrop is libre, 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. \
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!
## Contributing
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
[Contributing Guidelines](CONTRIBUTING.md).
================================================
FILE: dev/nginx/default.conf
================================================
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;
}
================================================
FILE: dev/nginx-with-openssl.Dockerfile
================================================
FROM nginx:alpine
RUN apk add --no-cache openssl
================================================
FILE: dev/openssl/create.sh
================================================
#!/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 "$@"
================================================
FILE: dev/openssl/pairdropCA.cnf
================================================
[ 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
================================================
FILE: dev/openssl/pairdropCert.cnf
================================================
[ 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
================================================
FILE: docker-compose-coturn.yml
================================================
version: "3"
services:
pairdrop:
image: "lscr.io/linuxserver/pairdrop:latest"
container_name: pairdrop
restart: unless-stopped
volumes:
- ./rtc_config.json:/home/node/app/rtc_config.json
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=/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:
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`
coturn_server:
image: "coturn/coturn"
restart: unless-stopped
volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf
- ./ssl/:/etc/coturn/ssl/
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
================================================
FILE: docker-compose-dev.yml
================================================
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
================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
pairdrop:
image: "lscr.io/linuxserver/pairdrop:latest"
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`
================================================
FILE: docs/docker-swarm-usage.md
================================================
# Docker Swarm Usage
## Healthcheck
The [Docker Image](../Dockerfile) includes a health check with the following options:
```
--interval=30s
```
> Specifies the time interval to run the health check. \
> In this case, the health check is performed every 30 seconds.
<br>
```
--timeout=10s
```
> Specifies the amount of time to wait for a response from the \"HEALTHCHECK\" command. \
> If the response does not arrive within 10 seconds, the health check fails.
<br>
```
--start-period=5s
```
> Specifies the amount of time to wait before starting the health check process. \
> In this case, the health check process will begin 5 seconds after the container is started.
<br>
```
--retries=3
```
> Specifies the number of times Docker should retry the health check \
> before considering the container to be unhealthy.
<br>
The CMD instruction is used to define the command that will be run as part of the health check. \
In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. \
This command will attempt to connect to `http://localhost:3000/` \
and if it fails it will exit with a status code of `1`. \
If this command returns a status code other than `0`, the health check fails.
Overall, this \"HEALTHCHECK\" instruction is defining a health check process \
that runs every 30 seconds, and waits up to 10 seconds for a response, \
begins 5 seconds after the container is started, and retries up to 3 times. \
The health check attempts to connect to http://localhost:3000/ \
and will considers the container unhealthy if unable to connect.
================================================
FILE: docs/faq.md
================================================
# Frequently Asked Questions
<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;">
Help! I can't install the PWA!
</summary>
<br>
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">
**Desktop 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>
**Self-Hosted Instance?** \
To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Shortcuts?
</summary>
<br>
Available shortcuts:
- Send a message with `CTRL + ENTER`
- Close all "Send" and "Pair" dialogs by pressing `Esc`.
- Copy a received message to the clipboard with `CTRL/⌘ + C`.
- Accept file-transfer requests with `Enter` and decline with `Esc`.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
How to save images directly to the gallery on iOS?
</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.~~
~~iOS Shortcuts saves the day:~~ \
I created a simple iOS shortcut that takes your photos and saves them to your gallery:
https://routinehub.co/shortcut/13988/
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>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Is it possible to send files or text directly from the "Context" or "Share" menu?
</summary>
<br>
Yes, it finally is.
* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Is it possible to send files or text directly via CLI?
</summary>
<br>
Yes.
* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Are there any third-party Apps?
</summary>
<br>
These third-party apps are compatible with PairDrop:
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
3. Feel free to make one :)
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server?
</summary>
<br>
It uses a WebRTC peer-to-peer connection.
WebRTC needs a signaling server that is only used to establish a connection.
The server is not involved in the file transfer.
If the devices are on the same network,
none of your files are ever sent to any server.
If your devices are paired and behind a NAT,
the PairDrop TURN Server is used to route your files and messages.
See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)
to learn more about STUN, TURN and WebRTC.
If you host your own instance
and want to support devices that do not support WebRTC,
you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about privacy? Will files be saved on third-party servers?
</summary>
<br>
Files are sent directly between peers.
PairDrop doesn't even use a database.
If curious, study [the signaling server](https://github.com/schlagmichdoch/PairDrop/blob/master/server/ws-server.js).
WebRTC encrypts the files in transit.
If the devices are on the same network,
none of your files are ever sent to any server.
If your devices are paired and behind a NAT,
the PairDrop TURN Server is used to route your files and messages.
See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)
to learn more about STUN, TURN and WebRTC.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
What about security? Are my files encrypted while sent between the computers?
</summary>
<br>
Yes. Your files are sent using WebRTC, encrypting them in transit.
Still you have to trust the PairDrop server. To ensure the connection is secure and there is no [MITM](https://en.m.wikipedia.org/wiki/Man-in-the-middle_attack) there is a plan to make PairDrop
zero trust by encrypting the signaling and implementing a verification process. See [issue #180](https://github.com/schlagmichdoch/PairDrop/issues/180) to keep updated.
<br>
</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;">
Transferring many files with paired devices takes too long
</summary>
<br>
Naturally, if traffic needs to be routed through the TURN server
because your devices are behind different NATs, transfer speed decreases.
You can open a hotspot on one of your devices to bridge the connection,
which omits the need of the TURN server.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
- [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot)
You can also use mobile hotspots on phones to do that.
Then, all data should be sent directly between devices and not use your data plan.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
Why don't you implement feature xyz?
</summary>
<br>
Snapdrop and PairDrop are a study in radical simplicity.
The user interface is insanely simple.
Features are chosen very carefully because complexity grows quadratically
since every feature potentially interferes with each other feature.
We focus very narrowly on a single use case: instant file transfer.
Not facilitating optimal edge-cases means better flow for average users.
Don't be sad. We may decline your feature request for the sake of simplicity.
Read *Insanely Simple: The Obsession that Drives Apple's Success*,
and/or *Thinking, Fast and Slow* to learn more.
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
PairDrop is awesome. How can I support it?
</summary>
<br>
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software.
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
* Share PairDrop on social media.
* Fix bugs and create a pull request.
* Do some security analysis and make suggestions.
* Participate in [active discussions](https://github.com/schlagmichdoch/PairDrop/discussions)
<br>
</details>
<details>
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
How does it work?
</summary>
<br>
[See here for info about the technical implementation](/docs/technical-documentation.md)
<br>
</details>
[< Back](/README.md)
================================================
FILE: docs/host-your-own.md
================================================
# Deployment Notes
## TURN server for Internet Transfer
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 (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
The easiest way to get PairDrop up and running is by using Docker.
### Docker Image from Docker Hub
```bash
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
<br>
### Docker Image from GitHub Container Registry (ghcr.io)
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop
```
<br>
### Docker Image self-built
#### Build the image
```bash
docker build --pull . -f Dockerfile -t pairdrop
```
> 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.
#### Run the image
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop
```
> 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 bypassing the proxy by reaching the docker container 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`
> - 8080 -> `-p 127.0.0.1:8080:3000`
#### Set Environment Variables via Docker
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>
## Deployment with Docker Compose
Here's an example docker compose file:
```yaml
version: "3"
services:
pairdrop:
image: "lscr.io/linuxserver/pairdrop:latest"
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
```
Run the compose file with `docker compose up -d`.
> 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 bypassing the proxy by reaching the Docker container
> directly, `127.0.0.1` is specified in the `ports` argument.
<br>
## Deployment with Node.js
Clone this repository and enter the folder
```bash
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop
```
Install all dependencies with NPM:
```bash
npm install
```
Start the server with:
```bash
npm start
```
> By default, the node server listens on port 3000.
<br>
### Options / Flags
These are some flags only reasonable when deploying via Node.js
#### 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
```bash
PORT=3000 RTC_CONFIG="rtc_config.json" npm start
```
On Windows
```bash
$env:PORT=3000 RTC_CONFIG="rtc_config.json"; npm start
```
Overview of available Environment Variables are found [here](#environment-variables).
<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
IPV6_LOCALIZE=4
```
> Default: `false`
>
> 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
WS_FALLBACK=true
```
> 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
RTC_CONFIG="rtc_config.json"
```
> Default: `false`
>
> Specify the STUN/TURN servers PairDrop clients use by setting \
> `RTC_CONFIG` to a JSON file including the configuration. \
> 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:
>
> ```json
> {
> "sdpSemantics": "unified-plan",
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> }
> ]
> }
> ```
<br>
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
SIGNALING_SERVER="pairdrop.net"
```
> Default: `false`
>
> By default, clients connecting to your instance use the signaling server of your instance to connect to other devices.
>
> By using `SIGNALING_SERVER`, you can host an instance that uses another signaling server.
>
> 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.
> E.g. host your own client files under *pairdrop.your-domain.com* but use the official signaling server under *pairdrop.net*
> 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.
>
> `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`
<br>
### Customizable buttons for the _About PairDrop_ page
```bash
DONATION_BUTTON_ACTIVE=true
DONATION_BUTTON_LINK="https://www.buymeacoffee.com/pairdrop"
DONATION_BUTTON_TITLE="Buy me a coffee"
TWITTER_BUTTON_ACTIVE=true
TWITTER_BUTTON_LINK="https://twitter.com/account"
TWITTER_BUTTON_TITLE="Find me on Twitter"
MASTODON_BUTTON_ACTIVE=true
MASTODON_BUTTON_LINK="https://mastodon.social/account"
MASTODON_BUTTON_TITLE="Find me on Mastodon"
BLUESKY_BUTTON_ACTIVE=true
BLUESKY_BUTTON_LINK="https://bsky.app/profile/account"
BLUESKY_BUTTON_TITLE="Find me on Bluesky"
CUSTOM_BUTTON_ACTIVE=true
CUSTOM_BUTTON_LINK="https://your-custom-social-network.net/account"
CUSTOM_BUTTON_TITLE="Find me on this custom social network"
PRIVACYPOLICY_BUTTON_ACTIVE=true
PRIVACYPOLICY_BUTTON_LINK="https://link-to-your-privacy-policy.net"
PRIVACYPOLICY_BUTTON_TITLE="Open our privacy policy"
```
> Default: unset
>
> By default, clients will show the default button configuration: GitHub, BuyMeACoffee, Twitter, and FAQ on GitHub.
>
> The GitHub and FAQ on GitHub buttons are essential, so they are always shown.
>
> 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
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \
Otherwise, all clients will be mutually visible.
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
### Using nginx
#### Allow http and https requests
```
server {
listen 80;
expires epoch;
location / {
proxy_connect_timeout 300;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-for $remote_addr;
}
}
server {
listen 443 ssl http2;
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://127.0.0.1:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-for $remote_addr;
}
}
```
#### Automatic http to https redirect:
```
server {
listen 80;
expires epoch;
location / {
return 301 https://$host:3000$request_uri;
}
}
server {
listen 443 ssl http2;
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://127.0.0.1:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-for $remote_addr;
}
}
```
<br>
### Using Apache
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
```bash
a2enmod proxy
```
```bash
a2enmod proxy_http
```
<br>
Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
**pairdrop.conf**
#### Allow HTTP and HTTPS requests
```apacheconf
<VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
</VirtualHost>
<VirtualHost *:443>
ProxyPass / https://127.0.0.1:3000/ upgrade=websocket
</VirtualHost>
```
#### Automatic HTTP to HTTPS redirect:
```apacheconf
<VirtualHost *:80>
Redirect permanent / https://127.0.0.1:3000/
</VirtualHost>
<VirtualHost *:443>
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
</VirtualHost>
```
Activate the new virtual host and reload Apache:
```bash
a2ensite pairdrop
```
```bash
service apache2 reload
```
<br>
## Coturn and PairDrop via Docker Compose
### 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:
```bash
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop
```
```bash
docker compose -f docker-compose-dev.yml up --no-deps --build
```
Now point your web browser to `http://localhost:8080`.
- To debug the Node.js server, run `docker logs pairdrop`.
- After changes to the code you have to rerun the `docker compose` command
<br>
#### Testing PWA related features
PWAs requires the app to be served under a correctly set up and trusted TLS endpoint.
The NGINX container creates a CA certificate and a website certificate for you.
To correctly set the common name of the certificate,
you need to change the FQDN environment variable in `docker-compose-dev.yml`
to the fully qualified domain name of your workstation. (Default: localhost)
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \
For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \
Install that certificate to the trust store of your operating system. \
##### Windows
- Make sure to install it to the `Trusted Root Certification Authorities` store.
##### macOS
- Double-click the installed CA certificate in `Keychain Access`,
- expand `Trust`, and select `Always Trust` for SSL.
##### Firefox
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.
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`).
[< Back](/README.md)
================================================
FILE: docs/how-to.md
================================================
# How-To
## Send directly from share menu on iOS
I created an iOS shortcut to send images, files, folder, URLs \
or text directly from the share-menu
https://routinehub.co/shortcut/13990/
[//]: # (Todo: Add screenshots)
<br>
## 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.
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 files or text with PairDrop via command-line interface. \
This opens PairDrop in the default browser where you can choose the receiver.
### Usage
```bash
pairdrop -h
```
```
Send files or text with PairDrop via command-line interface.
Current domain: https://pairdrop-dev.onrender.com/
Usage:
Open PairDrop: pairdrop
Send files: pairdrop file1/directory1 (file2/directory2 file3/directory3 ...)
Send text: pairdrop -t "text"
Specify domain: pairdrop -d "https://pairdrop.net/"
Show this help text: pairdrop (-h|--help)
This pairdrop-cli version was released alongside v1.10.4
```
<br>
### Setup
#### Linux / Mac
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)
```shell
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip"
```
or
```shell
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip"
```
2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`
```shell
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>
#### Windows
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)
2. Put file in a preferred folder e.g. `C:\Program Files\pairdrop-cli`
3. Inside this folder, copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_
4. Search for and open `Edit environment variables for your account`
5. Click `Environment Variables…`
6. Under _System Variables_ select `Path` and click _Edit..._
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)
================================================
FILE: docs/technical-documentation.md
================================================
# Technical Documentation
## Encryption, WebRTC, STUN and TURN
Encryption is mandatory for WebRTC connections and completely done by the browser itself.
When the peers are first connecting, \
a channel is created by exchanging their signaling info. \
This signaling information includes some sort of public key \
and is specific to the clients IP address. \
That is what the STUN Server is used for: \
it simply returns your public IP address \
as you only know your local ip address \
if behind a NAT (router).
The transfer of the signaling info is done by the \
PairDrop / Snapdrop server using secure websockets. \
After that the channel itself is completely peer-to-peer \
and all info can only be decrypted by the receiver. \
When the two peers are on the same network \
or when they are not behind any NAT system \
(which they are always for classic \
Snapdrop and for not paired users on PairDrop) \
the files are send directly peer-to-peer.
When a user is behind a NAT (behind a router) \
the contents are channeled through a TURN server. \
But again, the contents send via the channel \
can only be decrypted by the receiver. \
So a rogue TURN server could only \
see that there is a connection, but not what is sent. \
Obviously, connections which are channeled through a TURN server \
are not as fast as peer-to-peer.
The selection whether a TURN server is needed \
or not is also done automatically by the web browser. \
It simply iterated through the configured \
RTC iceServers and checks what works. \
Only if the STUN server is not sufficient, \
the TURN server is used.

_Diagram created by wowza.com_
Good thing: if your device has an IPv6 address \
it is uniquely reachable by that address. \
As I understand it, when both devices are using \
IPv6 addresses there is no need for a TURN server in any scenario.
Learn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \
which gives a good insight into STUN, TURN and WebRTC.
## Device Pairing
The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
It works by creating long secrets that are served \
by the server to the initiating and requesting pair peer, \
when the inserted key is correct. \
These long secrets are then saved to an \
indexedDB database in the web browser. \
IndexedDB is somewhat the successor of localStorage \
as saved data is shared between all tabs. \
It goes one step further by making the data persistent \
and available offline if implemented to a PWA.
All secrets a client has saved to its database \
are sent to the PairDrop server. \
Peers with a common secret are discoverable \
to each other analog to peers with the same \
IP address are discoverable by each other.
What I really like about this approach (and the reason I implemented it) \
is that devices on the same network are always \
visible regardless whether any devices are paired or not. \
The main user flow is never obstructed. \
Paired devices are simply shown additionally. \
This makes it in my idea better than the idea of \
using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).
[< Back](/README.md)
================================================
FILE: licenses/BSD_3-Clause-zip-js
================================================
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.
================================================
FILE: licenses/MIT-NoSleep
================================================
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.
================================================
FILE: licenses/MIT-heic2any
================================================
MIT License
Copyright (c) 2020 Alex Corvi
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: package.json
================================================
{
"name": "pairdrop",
"version": "1.11.2",
"type": "module",
"description": "",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
"start:prod": "node server/index.js --rate-limit --auto-restart"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"ua-parser-js": "^1.0.37",
"unique-names-generator": "^4.3.0",
"ws": "^8.16.0"
},
"engines": {
"node": ">=15"
}
}
================================================
FILE: pairdrop-cli/.gitignore
================================================
.pairdrop-cli-config
================================================
FILE: pairdrop-cli/.pairdrop-cli-config.example
================================================
DOMAIN=https://pairdrop.net/
================================================
FILE: pairdrop-cli/pairdrop
================================================
#!/bin/bash
set -e
# PairDrop version when this file was last changed
version="v1.10.4"
############################################################
# Help #
############################################################
help()
{
# Display Help
echo "Send files or text with PairDrop via command-line interface."
echo "Current domain: ${DOMAIN}"
echo
echo "Usage:"
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 text:\t\t$(basename "$0") -t \"text\""
echo -e "Specify domain:\t\t$(basename "$0") -d \"https://pairdrop.net/\""
echo -e "Show this help text:\t$(basename "$0") (-h|--help)"
echo
echo "This pairdrop-cli version was released alongside ${version}"
}
openPairDrop()
{
url="$DOMAIN"
if [[ -n $params ]];then
url="${url}?${params}"
fi
if [[ -n $hash ]];then
url="${url}#${hash}"
fi
echo "PairDrop is opening at $DOMAIN"
if [[ $OS == "Windows" ]];then
start "$url"
elif [[ $OS == "Mac" ]];then
open "$url"
elif [[ $OS == "WSL" || $OS == "WSL2" ]];then
powershell.exe /c "Start-Process ${url}"
else
xdg-open "$url" > /dev/null 2>&1
fi
exit
}
setOs()
{
unameOut=$(uname -a)
case "${unameOut}" in
*Microsoft*) OS="WSL";; #must be first since Windows subsystem for linux will have Linux in the name too
*microsoft*) OS="WSL2";; #WARNING: My v2 uses ubuntu 20.4 at the moment slightly different name may not always work
Linux*) OS="Linux";;
Darwin*) OS="Mac";;
CYGWIN*) OS="Cygwin";;
MINGW*) OS="Windows";;
*Msys) OS="Windows";;
*) OS="UNKNOWN:${unameOut}"
esac
}
specifyDomain()
{
[[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo "Incorrect format. Specify domain like https://pairdrop.net/" && exit
echo "DOMAIN=${1}" > "$config_path"
echo -e "Domain is now set to:\n$1\n"
}
sendText()
{
params="base64text=hash"
hash=$(echo -n "${OPTARG}" | base64)
if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then
params="base64text=paste"
if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then
echo -n "$hash" | clip.exe
elif [[ $OS == "Mac" ]];then
echo -n "$hash" | pbcopy
else
(echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli"
fi
hash=
fi
openPairDrop
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()
{
params="base64zip=hash"
workingDir="$(pwd)"
tmpDir="/tmp/pairdrop-cli-temp/"
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
# Workaround needed to create preferred zip structure on unix systems
# Create zip file with all single files by junking the path
if [[ $fileCount != 0 ]]; then
zip -q -b /tmp/ -j -0 -r "$zipToSendAbs" "${filePathsUnix[@]}"
fi
# Add directories recursively to zip file
index=0
while [[ $index < $directoryCount ]]; do
# workaround to keep directory name but junk the rest of the paths
# cd to path above directory
cd "${directoryPathsUnix[index]}"
# add directory to zip without junking the path
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
powershell.exe -Command "Compress-Archive -Path ${zipToSendBaseName} -DestinationPath ${wrapperZipAbsPS} -CompressionLevel Optimal"
else
zip -q -b /tmp/ -0 "$wrapperZipAbs" "$zipToSendBaseName"
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
# base64 encode zip file
if [[ $OS == "Mac" ]];then
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)
rm "$zipToSendAbs"
if [[ $(echo -n "$hash" | wc -m) -gt 1000 ]];then
params="base64zip=paste"
# Copy $hash to clipboard
if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then
echo -n "$hash" | clip.exe
elif [[ $OS == "Mac" ]];then
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
# X11
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
hash=
fi
openPairDrop
exit
}
############################################################
############################################################
# Main program #
############################################################
############################################################
script_path="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
pushd . > '/dev/null';
script_path="${BASH_SOURCE[0]:-$0}";
while [ -h "$script_path" ];
do
cd "$( dirname -- "$script_path"; )";
script_path="$( readlink -f -- "$script_path"; )";
done
cd "$( dirname -- "$script_path"; )" > '/dev/null';
script_path="$( pwd; )";
popd > '/dev/null';
config_path="${script_path}/.pairdrop-cli-config"
# If config file does not exist, try to create it. If it fails log error message and exit
[ ! -f "$config_path" ] &&
specifyDomain "https://pairdrop.net/" &&
[ ! -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 (${script_path})" &&
exit
# Read config variables
export "$(grep -v '^#' "$config_path" | xargs)"
setOs
############################################################
# Process the input options. Add options as needed. #
############################################################
# Get the options
# open PairDrop if no options are given
[[ $# -eq 0 ]] && openPairDrop && exit
# display help and exit if first argument is "--help" or more than 2 arguments are given
[ "$1" == "--help" ] && help && exit
while getopts "d:ht:*" option; do
case $option in
d) # specify domain - show help and exit if too many arguments
[[ $# -gt 2 ]] && help && exit
specifyDomain "$2"
exit;;
t) # Send text - show help and exit if too many arguments
[[ $# -gt 2 ]] && help && exit
sendText
exit;;
h | ?) # display help and exit
help
exit;;
esac
done
# Send file(s)
sendFiles "$@"
================================================
FILE: pairdrop-cli/pairdrop.sh
================================================
#!/bin/bash
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P )
cd "$parent_path" || exit
./pairdrop "$@"
================================================
FILE: pairdrop-cli/send-with-pairdrop
================================================
#!/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[@]}"
================================================
FILE: pairdrop-cli/send-with-pairdrop.ps1
================================================
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
& "$scriptDir\pairdrop.sh" $args
================================================
FILE: public/fonts/OpenSans/OFL.txt
================================================
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.
================================================
FILE: public/fonts/OpenSans/README.txt
================================================
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 aren’t 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.
================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="PairDrop">
<meta name="application-name" content="PairDrop">
<!-- Descriptions -->
<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="author" content="schlagmichdoch">
<meta property="og:title" content="PairDrop">
<meta property="og:type" content="article">
<meta property="og:url" content="https://pairdrop.net/">
<meta property="og:author" content="https://github.com/schlagmichdoch">
<meta name="twitter:author" content="@schlagmichdoch">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<meta name="og:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<!-- Icons -->
<link rel="icon" sizes="96x96" 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-precomposed" href="images/apple-touch-icon.png">
<meta name="msapplication-TileImage" content="images/mstile-150x150.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 property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources -->
<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/styles-main.css">
<link rel="manifest" href="manifest.json">
</head>
<body translate="no">
<header class="row-reverse wrap opacity-0">
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label">
<svg class="icon">
<use xlink:href="#info-outline"></use>
</svg>
</a>
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-language-selector"></use>
</svg>
</div>
<div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-auto"></use>
</svg>
</div>
<div>
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-light"></use>
</svg>
</div>
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#icon-theme-dark"></use>
</svg>
</div>
</div>
</div>
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#notifications"></use>
</svg>
</div>
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#homescreen"></use>
</svg>
</div>
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#pair-device-icon"></use>
</svg>
</div>
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#edit-pair-devices-icon"></use>
</svg>
</div>
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#public-room-icon"></use>
</svg>
</div>
<div id="expand" class="icon-button" data-i18n-key="header.expand" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#caret"></use>
</svg>
</div>
</header>
<!-- Center -->
<div id="center" class="opacity-0">
<!-- Peers -->
<x-peers class="center grow-5"></x-peers>
<x-no-peers class="center grow fade-in no-animation-on-load" data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg">
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text"></h2>
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text"></div>
</x-no-peers>
<x-instructions class="grow fade-in" data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"></x-instructions>
<div class="shr-panel panel column" hidden>
<div class="row">
<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>
<!-- Footer -->
<footer class="column opacity-0">
<svg class="icon logo">
<defs>
<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>
<div class="column">
<div class="known-as-wrapper">
<span data-i18n-key="footer.known-as" data-i18n-attrs="text"></span>
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg class="icon edit-pen">
<use xlink:href="#edit-pen-icon"></use>
</svg>
</div>
<div class="discovery-wrapper panel border row">
<div class="row center">
<span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span>
</div>
<div class="row center wrap">
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
</div>
</div>
</div>
</footer>
<!-- Language Select Dialog -->
<x-dialog id="language-select-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2>
</div>
<div class="language-buttons p2">
<button class="btn fw wrap">
<span data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></span>
</button>
<button class="btn fw wrap" value="ar">
<span lang="ar" dir="rtl">العربية</span>
<span> - </span>
<span>(Arabic)</span>
</button>
<button class="btn fw wrap" value="be">
<span lang="be">беларуская</span>
<span> - </span>
<span>(Belarusian)</span>
</button>
<button class="btn fw wrap" value="nb">
<span lang="nb">Bokmål</span>
<span> - </span>
<span>(Norwegian Bokmål)</span>
</button>
<button class="btn fw wrap" value="bg">
<span lang="bg">български</span>
<span> - </span>
<span>(Bulgarian)</span>
</button>
<button class="btn fw wrap" value="ca">
<span lang="ca">Català</span>
<span> - </span>
<span>(Catalan)</span>
</button>
<button class="btn fw wrap" value="cs">
<span lang="cs">Čeština</span>
<span> - </span>
<span>(Czech)</span>
</button>
<button class="btn fw wrap" value="da">
<span lang="da">Dansk</span>
<span> - </span>
<span>(Danish)</span>
</button>
<button class="btn fw wrap" value="de">
<span lang="de">Deutsch</span>
<span> - </span>
<span>(German)</span>
</button>
<button class="btn fw wrap" value="en">
<span lang="en">English</span>
</button>
<button class="btn fw wrap" value="es">
<span lang="es">Español</span>
<span> - </span>
<span>(Spanish)</span>
</button>
<button class="btn fw wrap" value="et">
<span lang="et">Eesti</span>
<span> - </span>
<span>(Estonian)</span>
</button>
<button class="btn fw wrap" value="eu">
<span lang="eu">Euskara</span>
<span> - </span>
<span>(Basque)</span>
</button>
<button class="btn fw wrap" value="fa">
<span lang="fa" dir="rtl">فارسی</span>
<span> - </span>
<span>(Persian)</span>
</button>
<button class="btn fw wrap" value="fr">
<span lang="fr">Français</span>
<span> - </span>
<span>(French)</span>
</button>
<button class="btn fw wrap" value="id">
<span lang="id">Bahasa Indonesia</span>
<span> - </span>
<span>(Indonesian)</span>
</button>
<button class="btn fw wrap" value="it">
<span lang="it">Italiano</span>
<span> - </span>
<span>(Italian)</span>
</button>
<button class="btn fw wrap" value="he">
<span lang="he" dir="rtl">עִבְרִית</span>
<span> - </span>
<span>(Hebrew)</span>
</button>
<button class="btn fw wrap" value="kn">
<span lang="kn">ಕನ್ನಡ</span>
<span> - </span>
<span>(Kannada)</span>
</button>
<button class="btn fw wrap" value="hu">
<span lang="hu">Magyar</span>
<span> - </span>
<span>(Hungarian)</span>
</button>
<button class="btn fw wrap" value="nl">
<span lang="nl">Nederlands</span>
<span> - </span>
<span>(Dutch)</span>
</button>
<button class="btn fw wrap" value="nn">
<span lang="nn">Norsk</span>
<span> - </span>
<span>(Norwegian Nynorsk)</span>
</button>
<button class="btn fw wrap" value="pl">
<span lang="pl">Polski</span>
<span> - </span>
<span>(Polish)</span>
</button>
<button class="btn fw wrap" value="pt-BR">
<span lang="pt-BR">Português do Brasil</span>
<span> - </span>
<span>(Brazilian Portuguese)</span>
</button>
<button class="btn fw wrap" value="ro">
<span lang="ro">Română</span>
<span> - </span>
<span>(Romanian)</span>
</button>
<button class="btn fw wrap" value="ru">
<span lang="ru">Русский язык</span>
<span> - </span>
<span>(Russian)</span>
</button>
<button class="btn fw wrap" value="sk">
<span lang="sk">Slovenčina</span>
<span> - </span>
<span>(Slovak)</span>
</button>
<button class="btn fw wrap" value="ta">
<span lang="ta">தமிழ்</span>
<span> - </span>
<span>(Tamil)</span>
</button>
<button class="btn fw wrap" value="tr">
<span lang="tr">Türkçe</span>
<span> - </span>
<span>(Turkish)</span>
</button>
<button class="btn fw wrap" value="uk">
<span lang="uk">Українська</span>
<span> - </span>
<span>(Ukrainian)</span>
</button>
<button class="btn fw wrap" value="zh-CN">
<span lang="zh-CN">汉语</span>
<span> - </span>
<span>(Simplified Chinese)</span>
</button>
<button class="btn fw wrap" value="zh-HK">
<span lang="zh-HK">中文</span>
<span> - </span>
<span>(Hant Script)</span>
</button>
<button class="btn fw wrap" value="zh-TW">
<span lang="zh-TW">漢語</span>
<span> - </span>
<span>(Traditional Chinese)</span>
</button>
<button class="btn fw wrap" value="ja">
<span lang="ja">日本語</span>
<span> - </span>
<span>(Japanese)</span>
</button>
<button class="btn fw wrap" value="ko">
<span lang="ko">한국어</span>
<span> - </span>
<span>(Korean)</span>
</button>
</div>
<div class="center row-reverse btn-row wrap">
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Pair Device Dialog -->
<x-dialog id="pair-device-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center p-2">
<div class="column">
<div class="center key-qr-code pointer" data-i18n-key="dialogs.pair-devices-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr">000 000</h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center p-2">
<div class="column fw">
<div class="input-key-container six-chars" dir="ltr">
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="btn-row row-reverse wrap">
<button class="btn btn-rounded btn-grey" 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>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text"></h2>
</div>
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty"></div>
<div class="row center p-2">
<div class="font-subheading">
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text"></span>
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text"></u>
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text"></span>
</div>
</div>
<div class="center row-reverse btn-row wrap">
<button class="btn btn-rounded btn-grey" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Public Room Dialog -->
<x-dialog id="public-room-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<div class="row center p-2">
<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="center key-qr-code pointer" data-i18n-key="dialogs.public-room-qr-code" data-i18n-attrs="title"></div>
<h1 class="center key" dir="ltr"></h1>
<p class="center text-center key-instructions">
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text"></span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text"></span>
</p>
</div>
</div>
<div class="hr-note">
<hr>
<div>
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center p-2">
<div class="column fw">
<div class="input-key-container" dir="ltr">
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder disabled>
</div>
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text"></p>
</div>
</div>
<div class="center row-reverse btn-row wrap">
<div class="row-reverse wrap grow-2">
<button class="btn btn-rounded btn-grey" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button>
<button class="btn btn-rounded btn-grey 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>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Request Dialog -->
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title"></h2>
</div>
<div class="row center p-2">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
</div>
<div class="center file-preview"></div>
<div class="row-reverse center btn-row wrap">
<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="decline-request" class="btn btn-rounded btn-grey" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Receive File Dialog -->
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title"></h2>
</div>
<div class="row center p-2">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
</div>
<div class="center file-preview"></div>
<div class="row-reverse center btn-row wrap">
<button id="share-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled></button>
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Send Text Dialog -->
<x-dialog id="send-text-dialog">
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center p-2 display-name-wrapper">
<div class="column">
<div class="text-center">
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text"></span>
<span class="display-name badge"></span>
</div>
</div>
</div>
<div class="row p-2">
<div class="column fw">
<div class="fw textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title placeholder" autofocus contenteditable></div>
</div>
</div>
<div class="btn-row row-reverse wrap">
<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="btn btn-rounded btn-grey" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Text Dialog -->
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title" class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center p-2 display-name-wrapper">
<div class="text-center">
<span class="display-name badge"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center p-2">
<div class="column fw">
<div id="text" class="textarea"></div>
</div>
</div>
<div class="row-reverse center btn-row wrap">
<button id="copy" class="btn btn-rounded btn-grey" 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>
</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>
</x-paper>
</x-background>
</x-dialog>
<!-- base64 Paste Dialog -->
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<div class="row center p-2">
<h2 class="dialog-title"></h2>
</div>
<div class="row p-2">
<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>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast id="toast" shadow="1">
<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>
<!-- About Page -->
<x-about id="about" class="full center column">
<header class="row-reverse">
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label">
<svg class="icon">
<use xlink:href="#close-icon"></use>
</svg>
</a>
</header>
<section class="center column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering"></use>
</svg>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.11.2</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#github"></use>
</svg>
</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">
<svg class="icon">
<use xlink:href="#donation"></use>
</svg>
</a>
<a class="icon-button" id="x-twitter-btn" target="_blank" href="https://x.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
<svg class="icon">
<use xlink:href="#x-twitter"></use>
</svg>
</a>
<a class="icon-button" id="mastodon-btn" target="_blank" rel="noreferrer" data-i18n-key="about.mastodon" data-i18n-attrs="title" hidden>
<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">
<svg class="icon">
<use xlink:href="#help-outline"></use>
</svg>
</a>
</div>
</section>
<x-background></x-background>
</x-about>
<canvas class="circles opacity-0"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id="wifi-tethering" viewBox="0 0 24 24">
<path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z"></path>
</symbol>
<symbol id="desktop-mac" viewBox="0 0 24 24">
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z"></path>
</symbol>
<symbol id="phone-iphone" viewBox="0 0 24 24">
<path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"></path>
</symbol>
<symbol id="tablet-mac" viewBox="0 0 24 24">
<path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z"></path>
</symbol>
<symbol id="info-outline" viewBox="0 0 24 24">
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
</symbol>
<symbol id="close-icon" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</symbol>
<symbol id="help-outline" viewBox="0 0 24 24">
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
</symbol>
<symbol id="x-twitter">
<path d="M17.996,2.219l3.265,0l-7.13,8.148l8.388,11.088l-6.566,0l-5.147,-6.723l-5.882,6.723l-3.269,0l7.625,-8.716l-8.041,-10.52l6.733,0l4.647,6.146l5.377,-6.146Zm-1.146,17.285l1.808,-0l-11.671,-15.435l-1.942,0l11.805,15.435Z"></path>
</symbol>
<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>
</symbol>
<symbol 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>
</symbol>
<symbol id="homescreen">
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path>
</symbol>
<symbol id="donation">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path>
</symbol>
<symbol id="icon-theme-auto" 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>
</symbol>
<symbol id="icon-theme-light" 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>
</symbol>
<symbol id="icon-theme-dark" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24"></rect><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path>
</symbol>
<symbol id="pair-device-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"></path>
</symbol>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"></path>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"></path>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"></path>
</symbol>
<symbol id="public-room-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"></path>
</symbol>
<symbol id="icon-language-selector" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"></path>
</symbol>
<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 512 512">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"/>
</symbol>
<symbol id="custom" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<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>
<!-- Scripts -->
<script src="scripts/localization.js" defer></script>
<script src="scripts/persistent-storage.js" defer></script>
<script src="scripts/ui-main.js" defer></script>
<script src="scripts/main.js" defer></script>
<!-- Sounds -->
<audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg">
<source src="sounds/blop.ogg" type="audio/ogg">
</audio>
<!-- no script -->
<noscript>
<x-noscript class="full center column">
<h1>Enable JavaScript</h1>
<h3>PairDrop works only with JavaScript</h3>
</x-noscript>
</noscript>
</body>
</html>
================================================
FILE: public/lang/ar.json
================================================
{
"footer": {
"webrtc": "إذا لم يكن WebRTC متاحًا.",
"public-room-devices_title": "يمكن اكتشافك بواسطة الأجهزة الموجودة في هذه الغرفة العامة المستقلة عن الشبكة.",
"display-name_data-placeholder": "تحميل …",
"display-name_title": "قم بتحرير اسم جهازك بشكل دائم",
"traffic": "حركة المرور هي",
"paired-devices_title": "يمكن اكتشافك بواسطة الأجهزة المقترنة في جميع الأوقات بشكل مستقل عن الشبكة.",
"public-room-devices": "في الغرفة {{roomId}}",
"paired-devices": "بواسطة الأجهزة المقترنة",
"on-this-network": "على هذه الشبكة",
"routed": "توجيهّا من خلال الخادم",
"discovery": "يمكنك اكتشافك:",
"on-this-network_title": "يمكن للجميع اكتشافك على هذه الشبكة.",
"known-as": "أنت معروف بأنك:"
},
"notifications": {
"request-title": "يرغب {{name}} في نقل {{count}} {{descriptor}}",
"unfinished-transfers-warning": "هناك تحويلات غير مكتملة. هل أنت متأكد أنك تريد إغلاق PairDrop؟",
"message-received": "تم استلام الرسالة بواسطة {{name}} - انقر للفتح",
"rate-limit-join-key": "تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.",
"connecting": "يتصل …",
"pairing-key-invalidated": "المفتاح {{key}} خاطئ",
"pairing-key-invalid": "مُفتاح خاطئ",
"connected": "متصل",
"pairing-not-persistent": "الأجهزة المقترنة ليست ثابتة",
"text-content-incorrect": "محتوى النص غير صحيح",
"message-transfer-completed": "اكتمل نقل الرسالة",
"file-transfer-completed": "اكتمل نقل الملف",
"file-content-incorrect": "محتوى الملف غير صحيح",
"files-incorrect": "الملفات غير صحيحة",
"selected-peer-left": "مُحَدد الاجهزة المقترنة",
"link-received": "تم استلام الرابط بواسطة {{name}} - انقر للفتح",
"online": "لقد عدت متصلاً بالإنترنت",
"public-room-left": "الخروج من الغرفة العامة {{publicRoomId}}",
"copied-text": "نُسِخَ النص إلى الحافظة",
"display-name-random-again": "يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى",
"display-name-changed-permanently": "يتم تغيير اسم العرض بشكل دائم",
"copied-to-clipboard-error": "النسخ غير ممكن. انسخ يدويًا.",
"pairing-success": "الأجهزة المقترنة",
"clipboard-content-incorrect": "محتوى الحافظة غير صحيح",
"display-name-changed-temporarily": "تم تغيير اسم العرض لهذه الجلسة فقط",
"copied-to-clipboard": "تم النسخ إلى الحافظة",
"offline": "انت غير متصل",
"pairing-tabs-error": "من المستحيل إقران علامتي تبويب متصفح الويب",
"public-room-id-invalid": "معرف الغرفة غير صالح",
"click-to-download": "إضغط للتحميل",
"pairing-cleared": "جميع الأجهزة غير مقترنة",
"notifications-enabled": "تم تمكين الإشعارات",
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة",
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة",
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
"download-successful": "تم تحميل {{descriptor}}",
"click-to-show": "اضغط للعرض",
"pair-url-copied-to-clipboard": "تم نسخ رابط اقتران هذا الجهاز إلى الحافظة",
"room-url-copied-to-clipboard": "تم نسخ رابط هذه الغرفة العامة إلى الحافظة",
"notifications-permissions-error": "لم يتم منح إذن الإشعارات حيث أن المستخدم أغلق نافذة السماح عدة مرات. يمكن إعادة تعيين هذا في معلومات الصفحة التي يمكن فتحها بالضغط على رمز القفل بجانب شريط عنوان صفحة الإنترنت."
},
"header": {
"cancel-share-mode": "تمّ",
"theme-auto_title": "تغيير المظهر تلقائيا من النظام",
"install_title": "تثبيت PairDrop",
"theme-dark_title": "إستخدم دائما المظهر المظلم",
"pair-device_title": "قم بإقران أجهزتك بشكل دائم",
"join-public-room_title": "انضم إلى الغرفة العامة مؤقتًا",
"notification_title": "تفعيل الإشعارات",
"edit-paired-devices_title": "عدل الأجهزة المقترنة",
"language-selector_title": "إختر اللغة",
"about_title": "حول PairDrop",
"about_aria-label": "افتح حول PairDrop",
"theme-light_title": "إستخدم دائماً المظهر الفاتح",
"edit-share-mode": "عدل",
"expand_title": "Expand header button row"
},
"instructions": {
"x-instructions_mobile": "انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة",
"x-instructions-share-mode_desktop": "انقر للإرسال {{descriptor}}",
"activate-share-mode-and-other-files-plural": "و{{count}} ملفات أخرى",
"x-instructions-share-mode_mobile": "انقر للإرسال {{descriptor}}",
"activate-share-mode-base": "افتح PairDrop على الأجهزة الأخرى للإرسال",
"no-peers-subtitle": "قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من أن تكتشف على الشبكات الأخرى",
"activate-share-mode-shared-text": "النص المشترك",
"x-instructions_desktop": "انقر لإرسال الملفات أو انقر بزر الفأرة الأيمن لإرسال رسالة",
"no-peers-title": "افتح PairDrop على الأجهزة الأخرى لإرسال الملفات",
"x-instructions_data-drop-bg": "حرر لتحديد المستلم",
"no-peers_data-drop-bg": "حرر لتحديد المستلم",
"x-instructions_data-drop-peer": "قم بالتحرير لإرسالها إلى القرين",
"activate-share-mode-and-other-file": "وملف واحد آخر",
"activate-share-mode-shared-files-plural": "{{count}} ملفات مشاركة",
"activate-share-mode-shared-file": "الملف المُشارك",
"webrtc-requirement": "لتستعمل بيردروب هنا، يجب أن يكون WebRTC مفعلًا!"
},
"peer-ui": {
"processing": "مُعالجة …",
"click-to-send-share-mode": "انقر للإرسال {{descriptor}}",
"click-to-send": "انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة",
"waiting": "يُرجى الإنتظار…",
"connection-hash": "للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين",
"preparing": "يقترن…",
"transferring": "جارٍ النقل…"
},
"dialogs": {
"base64-paste-to-send": "الصق هنا للمشاركة {{type}}",
"auto-accept-instructions-2": "لقبول جميع الملفات المرسلة من هذا الجهاز تلقائيًا.",
"receive-text-title": "تلقيت رسالة",
"edit-paired-devices-title": "تحرير الأجهزة المقترنة",
"cancel": "إلغاء",
"auto-accept-instructions-1": "تفعيل",
"pair-devices-title": "إقران الأجهزة بشكل دائم",
"download": "تحميل",
"title-file": "ملف",
"base64-processing": "مُعالجة…",
"decline": "رفض",
"receive-title": "تم استلام {{descriptor}}",
"leave": "مُغادرة",
"join": "انضمام",
"title-image-plural": "صور",
"send": "ارسال",
"base64-tap-to-paste": "انقر هنا للمشاركة {{type}}",
"base64-text": "نص",
"copy": "نسخ",
"file-other-description-image": "وصورة واحدة أخرى",
"temporary-public-room-title": "غرفة عامة مؤقتة",
"base64-files": "ملفات",
"has-sent": "ارسلت:",
"file-other-description-file": "وملف واحد آخر",
"close": "إغلاق",
"system-language": "لغة النظام",
"unpair": "إلغاء الإقتران",
"title-image": "صورة",
"file-other-description-file-plural": "و{{count}} ملفات أخرى",
"would-like-to-share": "ترغب في المشاركة",
"send-message-to": "أرسال رسالة إلى:",
"language-selector-title": "إختر اللُغة",
"pair": "إقتران",
"hr-or": "او",
"scan-qr-code": "أو مسح رمز الاستجابة السريعة.",
"input-key-on-this-device": "أدخل هذا المفتاح على جهاز آخر",
"download-again": "تحميل مرة أخرى",
"accept": "قبول",
"paired-devices-wrapper_data-empty": "لا توجد أجهزة مقترنة.",
"enter-key-from-another-device": "أدخل المفتاح من جهاز آخر هنا.",
"share": "مُشاركة",
"auto-accept": "قبول تلقائي",
"title-file-plural": "ملفات",
"send-message-title": "إرسال رسالة",
"input-room-id-on-another-device": "أدخل معرف الغرفة هذا على جهاز آخر",
"file-other-description-image-plural": "و{{count}} صور أخرى",
"enter-room-id-from-another-device": "أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة.",
"share-text-title": "شارك رسالة نصية",
"paired-device-removed": "تمت إزالة الجهاز المقترن.",
"message_title": "أدخل رسالة لإرسالها",
"message_placeholder": "النص",
"base64-title-files": "شارك ملفات",
"base64-title-text": "شارك نصًا",
"public-room-qr-code_title": "اضغط لنسخ رابط الغرفة العامة",
"approve": "قبول",
"share-text-subtitle": "عدل الرسالة قبل الإرسال:",
"share-text-checkbox": "أظهر هذه الرسالة دائمًا عند مشاركة النصوص",
"close-toast_title": "أغلق الإشعار",
"pair-devices-qr-code_title": "اضغط لنسخ رابط اقتران هذا الجهاز"
},
"about": {
"claim": "أسهل طريقة لنقل الملفات عبر الأجهزة",
"tweet_title": "غرّد حول PairDrop",
"close-about_aria-label": "إغلاق حول PairDrop",
"buy-me-a-coffee_title": "اشتري لي القهوة!",
"github_title": "PairDrop على جيت هاب",
"faq_title": "أسئلة متكررة",
"mastodon_title": "اكتب عن بيردروب على ماستادون",
"bluesky_title": "تابعنا على بلوسكاي",
"custom_title": "تابعنا",
"privacypolicy_title": "افتح سياسة الخصوصية الخاصة بنا"
},
"document-titles": {
"file-transfer-requested": "طلب نقل الملف",
"message-received-plural": "{{count}} الرسائل المستلمة",
"message-received": "تم إرسال الرسالة",
"file-received": "تم استلام الملف",
"file-received-plural": "{{count}} الملفات المستلمة",
"image-transfer-requested": "طُلب نقل الصور المطلوبة"
}
}
================================================
FILE: public/lang/be.json
================================================
{
"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": "Вас можа знайсці кожны ў гэтай
gitextract_eu7hkmeg/ ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── enhancement.md │ ├── dependabot.yml │ └── workflows/ │ ├── docker-image.yml │ ├── github-image.yml │ └── zip-release.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── dev/ │ ├── nginx/ │ │ └── default.conf │ ├── nginx-with-openssl.Dockerfile │ └── openssl/ │ ├── create.sh │ ├── pairdropCA.cnf │ └── pairdropCert.cnf ├── docker-compose-coturn.yml ├── docker-compose-dev.yml ├── docker-compose.yml ├── docs/ │ ├── docker-swarm-usage.md │ ├── faq.md │ ├── host-your-own.md │ ├── how-to.md │ └── technical-documentation.md ├── licenses/ │ ├── BSD_3-Clause-zip-js │ ├── MIT-NoSleep │ └── MIT-heic2any ├── package.json ├── pairdrop-cli/ │ ├── .gitignore │ ├── .pairdrop-cli-config.example │ ├── pairdrop │ ├── pairdrop.sh │ ├── send with PairDrop.lnk │ ├── send-with-pairdrop │ └── send-with-pairdrop.ps1 ├── public/ │ ├── fonts/ │ │ └── OpenSans/ │ │ ├── OFL.txt │ │ └── README.txt │ ├── images/ │ │ └── snapdrop-graphics.sketch │ ├── index.html │ ├── lang/ │ │ ├── ar.json │ │ ├── be.json │ │ ├── bg.json │ │ ├── bn.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── da.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── et.json │ │ ├── eu.json │ │ ├── fa.json │ │ ├── fi.json │ │ ├── fr.json │ │ ├── he.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── kab.json │ │ ├── kn.json │ │ ├── ko.json │ │ ├── nb.json │ │ ├── nl.json │ │ ├── nn.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── sk.json │ │ ├── ta.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── zh-CN.json │ │ ├── zh-HK.json │ │ └── zh-TW.json │ ├── manifest.json │ ├── robots.txt │ ├── scripts/ │ │ ├── browser-tabs-connector.js │ │ ├── localization.js │ │ ├── main.js │ │ ├── network.js │ │ ├── persistent-storage.js │ │ ├── ui-main.js │ │ ├── ui.js │ │ ├── util.js │ │ └── worker/ │ │ └── canvas-worker.js │ ├── service-worker.js │ ├── sounds/ │ │ └── blop.ogg │ └── styles/ │ ├── styles-deferred.css │ └── styles-main.css ├── rtc_config_example.json ├── server/ │ ├── helper.js │ ├── index.js │ ├── peer.js │ ├── server.js │ └── ws-server.js └── turnserver_example.conf
SYMBOL INDEX (530 symbols across 13 files)
FILE: public/scripts/browser-tabs-connector.js
class BrowserTabsConnector (line 1) | class BrowserTabsConnector {
method constructor (line 2) | constructor() {
method _broadcastSend (line 10) | _broadcastSend(message) {
method _onMessage (line 14) | _onMessage(e) {
method peerIsSameBrowser (line 23) | static peerIsSameBrowser(peerId) {
method addPeerIdToLocalStorage (line 30) | static async addPeerIdToLocalStorage() {
method removePeerIdFromLocalStorage (line 45) | static async removePeerIdFromLocalStorage(peerId) {
method removeOtherPeerIdsFromLocalStorage (line 54) | static async removeOtherPeerIdsFromLocalStorage() {
FILE: public/scripts/localization.js
class Localization (line 1) | class Localization {
method constructor (line 2) | constructor() {
method localeIsSupported (line 24) | static localeIsSupported(locale) {
method localeIsRtl (line 28) | static localeIsRtl(locale) {
method currentLocaleIsRtl (line 32) | static currentLocaleIsRtl() {
method currentLocaleIsDefault (line 36) | static currentLocaleIsDefault() {
method getSupportedOrDefaultLocales (line 40) | static getSupportedOrDefaultLocales(locales) {
method setInitialTranslation (line 53) | async setInitialTranslation() {
method setTranslation (line 58) | static async setTranslation(locale) {
method fetchDefaultTranslations (line 82) | static async fetchDefaultTranslations() {
method fetchTranslations (line 86) | static async fetchTranslations(newLocale) {
method getLocale (line 97) | static getLocale() {
method isSystemLocale (line 101) | static isSystemLocale() {
method fetchTranslationsFor (line 105) | static async fetchTranslationsFor(newLocale) {
method translatePage (line 117) | static async translatePage() {
method translateElement (line 123) | static async translateElement(element) {
method getTranslationFromTranslationsObj (line 137) | static getTranslationFromTranslationsObj(translationObj, key, attr) {
method addDataToTranslation (line 164) | static addDataToTranslation(translation, data) {
method getTranslation (line 175) | static getTranslation(key, attr = null, data = {}, useDefault = false) {
method logTranslationMissingOrBroken (line 209) | static logTranslationMissingOrBroken(key, attr, data, useDefault) {
method logHelpCall (line 217) | static logHelpCall() {
method logHelpCallKey (line 221) | static logHelpCallKey(key, attr) {
method escapeHTML (line 231) | static escapeHTML(unsafeText) {
FILE: public/scripts/main.js
class PairDrop (line 1) | class PairDrop {
method constructor (line 3) | constructor() {
method initialize (line 41) | async initialize() {
method registerServiceWorker (line 72) | registerServiceWorker() {
method onPwaInstallable (line 83) | onPwaInstallable(e) {
method evaluatePermissionsAndRoomSecrets (line 95) | async evaluatePermissionsAndRoomSecrets() {
method loadDeferredAssets (line 108) | loadDeferredAssets() {
method loadStyleSheet (line 115) | loadStyleSheet(url) {
method loadAndApplyStylesheet (line 132) | loadAndApplyStylesheet(url) {
method loadScript (line 144) | loadScript(url) {
method loadAndApplyScript (line 155) | loadAndApplyScript(url) {
method hydrate (line 167) | async hydrate() {
method evaluateUrlParams (line 191) | async evaluateUrlParams() {
FILE: public/scripts/network.js
class ServerConnection (line 1) | class ServerConnection {
method constructor (line 3) | constructor() {
method _getConfig (line 29) | _getConfig() {
method _setWsConfig (line 65) | _setWsConfig(wsConfig) {
method _connect (line 70) | _connect() {
method _onOpen (line 88) | _onOpen() {
method _onPairDeviceInitiate (line 94) | _onPairDeviceInitiate() {
method _onPairDeviceJoin (line 102) | _onPairDeviceJoin(pairKey) {
method _onCreatePublicRoom (line 110) | _onCreatePublicRoom() {
method _onJoinPublicRoom (line 118) | _onJoinPublicRoom(roomId, createIfInvalid) {
method _onLeavePublicRoom (line 126) | _onLeavePublicRoom() {
method _onMessage (line 134) | _onMessage(msg) {
method send (line 213) | send(msg) {
method _onPeers (line 219) | _onPeers(msg) {
method _onDisplayName (line 223) | _onDisplayName(msg) {
method _endpoint (line 246) | _endpoint() {
method _disconnect (line 267) | _disconnect() {
method _onDisconnect (line 286) | _onDisconnect() {
method _onVisibilityChange (line 295) | _onVisibilityChange() {
method _isConnected (line 300) | _isConnected() {
method _isConnecting (line 304) | _isConnecting() {
method _isOffline (line 308) | _isOffline() {
method _onError (line 312) | _onError(e) {
method _reconnect (line 316) | _reconnect() {
class Peer (line 322) | class Peer {
method constructor (line 324) | constructor(serverConnection, isCaller, peerId, roomType, roomId) {
method sendJSON (line 339) | sendJSON(message) {
method _send (line 344) | _send(message) {}
method sendDisplayName (line 346) | sendDisplayName(displayName) {
method _isSameBrowser (line 350) | _isSameBrowser() {
method _isPaired (line 354) | _isPaired() {
method _getPairSecret (line 358) | _getPairSecret() {
method _regenerationOfPairSecretNeeded (line 362) | _regenerationOfPairSecretNeeded() {
method _getRoomTypes (line 366) | _getRoomTypes() {
method _updateRoomIds (line 370) | _updateRoomIds(roomType, roomId) {
method _removeRoomType (line 402) | _removeRoomType(roomType) {
method _evaluateAutoAccept (line 411) | _evaluateAutoAccept() {
method _setAutoAccept (line 430) | _setAutoAccept(autoAccept) {
method requestFileTransfer (line 436) | async requestFileTransfer(files) {
method sendFiles (line 475) | async sendFiles() {
method _dequeueFile (line 484) | _dequeueFile() {
method _sendFile (line 490) | async _sendFile(file) {
method _onPartitionEnd (line 503) | _onPartitionEnd(offset) {
method _onReceivedPartitionEnd (line 507) | _onReceivedPartitionEnd(offset) {
method _sendNextPartition (line 511) | _sendNextPartition() {
method _sendProgress (line 516) | _sendProgress(progress) {
method _onMessage (line 520) | _onMessage(message) {
method _onFilesTransferRequest (line 560) | _onFilesTransferRequest(request) {
method _respondToFileTransferRequest (line 588) | _respondToFileTransferRequest(accepted) {
method _onFileHeader (line 599) | _onFileHeader(header) {
method _abortTransfer (line 610) | _abortTransfer() {
method _onChunkReceived (line 619) | _onChunkReceived(chunk) {
method _onDownloadProgress (line 637) | _onDownloadProgress(progress) {
method _onFileReceived (line 641) | async _onFileReceived(fileBlob) {
method _onFileTransferCompleted (line 666) | _onFileTransferCompleted() {
method _onFileTransferRequestResponded (line 678) | _onFileTransferRequestResponded(message) {
method _onMessageTransferCompleted (line 692) | _onMessageTransferCompleted() {
method sendText (line 696) | sendText(text) {
method _onTextReceived (line 701) | _onTextReceived(message) {
method _onDisplayNameChanged (line 708) | _onDisplayNameChanged(message) {
class RTCPeer (line 722) | class RTCPeer extends Peer {
method constructor (line 724) | constructor(serverConnection, isCaller, peerId, roomType, roomId, rtcC...
method _connect (line 734) | _connect() {
method _openConnection (line 745) | _openConnection() {
method _openChannel (line 753) | _openChannel() {
method _onDescription (line 769) | _onDescription(description) {
method _onIceCandidate (line 777) | _onIceCandidate(event) {
method onServerMessage (line 782) | onServerMessage(message) {
method _onChannelOpened (line 804) | _onChannelOpened(event) {
method _onMessage (line 816) | _onMessage(message) {
method getConnectionHash (line 823) | getConnectionHash() {
method _onBeforeUnload (line 849) | _onBeforeUnload(e) {
method _onPageHide (line 856) | _onPageHide() {
method _disconnect (line 860) | _disconnect() {
method _onChannelClosed (line 868) | _onChannelClosed() {
method _onConnectionStateChange (line 875) | _onConnectionStateChange() {
method _onIceConnectionStateChange (line 889) | _onIceConnectionStateChange() {
method _onError (line 899) | _onError(error) {
method _send (line 903) | _send(message) {
method _sendSignal (line 908) | _sendSignal(signal) {
method refresh (line 916) | refresh() {
method _isConnected (line 926) | _isConnected() {
method _isConnecting (line 930) | _isConnecting() {
method sendDisplayName (line 934) | sendDisplayName(displayName) {
class WSPeer (line 940) | class WSPeer extends Peer {
method constructor (line 942) | constructor(serverConnection, isCaller, peerId, roomType, roomId) {
method _send (line 951) | _send(chunk) {
method sendJSON (line 958) | sendJSON(message) {
method _sendSignal (line 965) | _sendSignal(connected = false) {
method onServerMessage (line 969) | onServerMessage(message) {
method getConnectionHash (line 976) | getConnectionHash() {
class PeersManager (line 982) | class PeersManager {
method constructor (line 984) | constructor(serverConnection) {
method _onWsConfig (line 1014) | _onWsConfig(wsConfig) {
method _onMessage (line 1018) | _onMessage(message) {
method _refreshPeer (line 1023) | _refreshPeer(peerId, roomType, roomId) {
method _createOrRefreshPeer (line 1044) | _createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
method _onPeerJoined (line 1062) | _onPeerJoined(message) {
method _onPeers (line 1066) | _onPeers(message) {
method _onWsRelay (line 1072) | _onWsRelay(message) {
method _onRespondToFileTransferRequest (line 1080) | _onRespondToFileTransferRequest(detail) {
method _onFilesSelected (line 1084) | async _onFilesSelected(message) {
method _onSendText (line 1089) | _onSendText(message) {
method _onPeerLeft (line 1093) | _onPeerLeft(message) {
method _onPeerConnected (line 1114) | _onPeerConnected(peerId) {
method _peerExists (line 1118) | _peerExists(peerId) {
method _webRtcSupported (line 1122) | _webRtcSupported(peerId) {
method _onWsDisconnected (line 1126) | _onWsDisconnected() {
method _onPeerDisconnected (line 1136) | _onPeerDisconnected(peerId) {
method _onRoomSecretsDeleted (line 1146) | _onRoomSecretsDeleted(roomSecrets) {
method _onLeavePublicRoom (line 1152) | _onLeavePublicRoom(publicRoomId) {
method _onSecretRoomDeleted (line 1156) | _onSecretRoomDeleted(roomSecret) {
method _disconnectOrRemoveRoomTypeByRoomId (line 1160) | _disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
method _disconnectOrRemoveRoomTypeByPeerId (line 1170) | _disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
method _onRoomSecretRegenerated (line 1183) | _onRoomSecretRegenerated(message) {
method _notifyPeersDisplayNameChanged (line 1192) | _notifyPeersDisplayNameChanged(newDisplayName) {
method _notifyPeerDisplayNameChanged (line 1199) | _notifyPeerDisplayNameChanged(peerId) {
method _onDisplayName (line 1205) | _onDisplayName(displayName) {
method _onAutoAcceptUpdated (line 1211) | _onAutoAcceptUpdated(roomSecret, autoAccept) {
method _getPeerIdsFromRoomId (line 1219) | _getPeerIdsFromRoomId(roomId) {
class FileChunker (line 1235) | class FileChunker {
method constructor (line 1237) | constructor(file, onChunk, onPartitionEnd) {
method nextPartition (line 1249) | nextPartition() {
method _readChunk (line 1254) | _readChunk() {
method _onChunkRead (line 1259) | _onChunkRead(chunk) {
method repeatPartition (line 1271) | repeatPartition() {
method _isPartitionEnd (line 1276) | _isPartitionEnd() {
method isFileEnd (line 1280) | isFileEnd() {
class FileDigester (line 1285) | class FileDigester {
method constructor (line 1287) | constructor(meta, totalSize, totalBytesReceived, callback) {
method unchunk (line 1298) | unchunk(chunk) {
FILE: public/scripts/persistent-storage.js
class PersistentStorage (line 1) | class PersistentStorage {
method constructor (line 2) | constructor() {
method logBrowserNotCapable (line 56) | static logBrowserNotCapable() {
method set (line 60) | static set(key, value) {
method get (line 79) | static get(key) {
method delete (line 98) | static delete(key) {
method addRoomSecret (line 117) | static addRoomSecret(roomSecret, displayName, deviceName) {
method getAllRoomSecrets (line 141) | static async getAllRoomSecrets() {
method getAllRoomSecretEntries (line 156) | static getAllRoomSecretEntries() {
method getRoomSecretEntry (line 174) | static getRoomSecretEntry(roomSecret) {
method deleteRoomSecret (line 208) | static deleteRoomSecret(roomSecret) {
method clearRoomSecrets (line 239) | static clearRoomSecrets() {
method updateRoomSecretNames (line 258) | static updateRoomSecretNames(roomSecret, displayName, deviceName) {
method updateRoomSecretAutoAccept (line 262) | static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
method updateRoomSecret (line 266) | static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, upd...
FILE: public/scripts/ui-main.js
class Events (line 6) | class Events {
method fire (line 7) | static fire(type, detail = {}) {
method on (line 11) | static on(type, callback, options) {
method off (line 15) | static off(type, callback, options) {
class ThemeUI (line 21) | class ThemeUI {
method constructor (line 23) | constructor() {
method getCurrentTheme (line 43) | getCurrentTheme() {
method setCurrentTheme (line 47) | setCurrentTheme(theme) {
method onClickAuto (line 51) | onClickAuto() {
method onClickLight (line 59) | onClickLight() {
method onClickDark (line 67) | onClickDark() {
method setModeToDark (line 75) | setModeToDark() {
method setModeToLight (line 86) | setModeToLight() {
method setModeToAuto (line 97) | setModeToAuto() {
class HeaderUI (line 114) | class HeaderUI {
method constructor (line 116) | constructor() {
method fadeIn (line 123) | async fadeIn() {
method evaluateOverflowing (line 127) | async evaluateOverflowing() {
method onExpandBtnClick (line 163) | onExpandBtnClick() {
class CenterUI (line 179) | class CenterUI {
method constructor (line 181) | constructor() {
method fadeIn (line 186) | async fadeIn() {
class FooterUI (line 196) | class FooterUI {
method constructor (line 198) | constructor() {
method showLoading (line 212) | async showLoading() {
method fadeIn (line 216) | async fadeIn() {
method _evaluateFooterBadges (line 220) | async _evaluateFooterBadges() {
method _loadSavedDisplayName (line 232) | async _loadSavedDisplayName() {
method _onDisplayName (line 241) | async _onDisplayName(displayNameServer){
method _insertDisplayName (line 250) | _insertDisplayName(displayName) {
method _onKeyDownDisplayName (line 254) | _onKeyDownDisplayName(e) {
method _onFocusDisplayName (line 261) | _onFocusDisplayName(e) {
method _onBlurDisplayName (line 272) | async _onBlurDisplayName(e) {
method _saveDisplayName (line 284) | async _saveDisplayName(newDisplayName) {
method _getSavedDisplayName (line 318) | _getSavedDisplayName() {
class BackgroundCanvas (line 334) | class BackgroundCanvas {
method constructor (line 335) | constructor() {
method fadeIn (line 342) | async fadeIn() {
method initAnimation (line 346) | initAnimation() {
method initAnimationOnscreen (line 380) | initAnimationOnscreen() {
method initAnimationOffscreen (line 513) | initAnimationOffscreen() {
FILE: public/scripts/ui.js
class PeersUI (line 1) | class PeersUI {
method constructor (line 3) | constructor() {
method _evaluateRtcSupport (line 56) | _evaluateRtcSupport(wsConfig) {
method _changePeerDisplayName (line 68) | _changePeerDisplayName(peerId, displayName) {
method _onPeerDisplayNameChanged (line 75) | _onPeerDisplayNameChanged(e) {
method _onKeyDown (line 80) | async _onKeyDown(e) {
method _onPeerJoined (line 93) | _onPeerJoined(msg) {
method _joinPeer (line 97) | _joinPeer(peer, roomType, roomId) {
method _onPeerConnected (line 113) | _onPeerConnected(peerId, connectionHash) {
method _redrawPeerRoomTypes (line 124) | _redrawPeerRoomTypes(peerId) {
method _evaluateOverflowingPeers (line 139) | _evaluateOverflowingPeers() {
method _onPeers (line 148) | _onPeers(msg) {
method _onPeerDisconnected (line 152) | _onPeerDisconnected(peerId) {
method _onRoomTypeRemoved (line 166) | _onRoomTypeRemoved(peerId, roomType) {
method _onSetProgress (line 176) | _onSetProgress(progress) {
method _onDrop (line 182) | _onDrop(e) {
method _onDragOver (line 209) | _onDragOver(e) {
method _onDragEnd (line 218) | _onDragEnd() {
method _onPaste (line 223) | _onPaste(e) {
method _activateShareMode (line 245) | async _activateShareMode(files = [], text = "") {
method _reloadShareMode (line 342) | async _reloadShareMode() {
method _deactivateShareMode (line 353) | async _deactivateShareMode() {
method _sendShareData (line 386) | _sendShareData(e) {
class PeerUI (line 407) | class PeerUI {
method constructor (line 409) | constructor(peer, connectionHash, shareMode) {
method html (line 432) | html() {
method addTypesToClassList (line 469) | addTypesToClassList() {
method _initDom (line 479) | _initDom() {
method _onShareModeChanged (line 495) | _onShareModeChanged(active = false, descriptor = "") {
method _evaluateShareMode (line 504) | _evaluateShareMode() {
method _createCallbacks (line 517) | _createCallbacks() {
method _bindListeners (line 531) | _bindListeners() {
method _onPointerDown (line 565) | _onPointerDown(e) {
method _displayName (line 574) | _displayName() {
method _deviceName (line 578) | _deviceName() {
method _badgeClassName (line 582) | _badgeClassName() {
method _icon (line 591) | _icon() {
method _onFilesSelected (line 602) | _onFilesSelected(e) {
method setProgress (line 615) | setProgress(progress, status) {
method _onDrop (line 647) | _onDrop(e) {
method _onDragOver (line 672) | _onDragOver() {
method _onDragEnd (line 677) | _onDragEnd() {
method _onRightClick (line 682) | _onRightClick(e) {
method _onTouchStart (line 690) | _onTouchStart(e) {
method _onTouchEnd (line 695) | _onTouchEnd(e) {
class Dialog (line 710) | class Dialog {
method constructor (line 711) | constructor(id) {
method anyDialogShown (line 724) | static anyDialogShown() {
method show (line 728) | show() {
method isShown (line 740) | isShown() {
method hide (line 744) | hide() {
method _onPeerDisconnected (line 755) | _onPeerDisconnected(peerId) {
method _evaluateOverflowing (line 762) | _evaluateOverflowing(element) {
class LanguageSelectDialog (line 772) | class LanguageSelectDialog extends Dialog {
method constructor (line 774) | constructor() {
method _onKeyDown (line 787) | _onKeyDown(e) {
method show (line 795) | show() {
method hide (line 806) | hide() {
method selectLanguage (line 812) | selectLanguage(e) {
class ReceiveDialog (line 828) | class ReceiveDialog extends Dialog {
method constructor (line 829) | constructor(id) {
method _formatFileSize (line 841) | _formatFileSize(bytes) {
method _parseFileData (line 858) | _parseFileData(displayName, connectionHash, files, imagesOnly, totalSi...
class ReceiveFileDialog (line 889) | class ReceiveFileDialog extends ReceiveDialog {
method constructor (line 891) | constructor() {
method _onFilesReceived (line 901) | async _onFilesReceived(peerId, files, imagesOnly, totalSize) {
method _nextFiles (line 921) | async _nextFiles() {
method createPreviewElement (line 928) | createPreviewElement(file) {
method _displayFiles (line 963) | async _displayFiles(peerId, displayName, connectionHash, files, images...
method _downloadFilesIndividually (line 1080) | _downloadFilesIndividually(files) {
method hide (line 1089) | hide() {
class ReceiveRequestDialog (line 1101) | class ReceiveRequestDialog extends ReceiveDialog {
method constructor (line 1103) | constructor() {
method _onKeyDown (line 1116) | _onKeyDown(e) {
method _onRequestFileTransfer (line 1124) | _onRequestFileTransfer(request, peerId) {
method _dequeueRequests (line 1130) | _dequeueRequests() {
method _showRequestDialog (line 1136) | _showRequestDialog(request, peerId) {
method _respondToFileTransferRequest (line 1165) | _respondToFileTransferRequest(accepted) {
method hide (line 1177) | hide() {
class InputKeyContainer (line 1191) | class InputKeyContainer {
method constructor (line 1192) | constructor(inputKeyContainer, evaluationRegex, onAllCharsFilled, onNo...
method _enableChars (line 1210) | _enableChars() {
method _disableChars (line 1214) | _disableChars() {
method _clearChars (line 1218) | _clearChars() {
method _cleanUp (line 1222) | _cleanUp() {
method _onCharsInput (line 1227) | _onCharsInput(e) {
method _onCharsKeyDown (line 1241) | _onCharsKeyDown(e) {
method _onCharsKeyUp (line 1258) | _onCharsKeyUp(e) {
method _getInputKey (line 1265) | _getInputKey() {
method _onPaste (line 1273) | _onPaste(pastedKey) {
method _evaluateKeyChars (line 1285) | _evaluateKeyChars() {
method focusLastChar (line 1299) | focusLastChar() {
class PairDeviceDialog (line 1305) | class PairDeviceDialog extends Dialog {
method constructor (line 1306) | constructor() {
method _onKeyDown (line 1346) | _onKeyDown(e) {
method _onPaste (line 1355) | _onPaste(e) {
method _pairDeviceInitiate (line 1364) | _pairDeviceInitiate() {
method _onPairDeviceInitiated (line 1368) | _onPairDeviceInitiated(msg) {
method _setKeyAndQRCode (line 1376) | _setKeyAndQRCode() {
method _getPairUrl (line 1393) | _getPairUrl() {
method _copyPairUrl (line 1399) | _copyPairUrl() {
method _onSubmit (line 1409) | _onSubmit(e) {
method _submit (line 1414) | _submit() {
method _pairDeviceJoin (line 1419) | _pairDeviceJoin(pairKey) {
method _onPairDeviceJoined (line 1426) | _onPairDeviceJoined(peerId, roomSecret) {
method _onPeers (line 1445) | _onPeers(message) {
method _onPeerJoined (line 1451) | _onPeerJoined(message) {
method _evaluateJoinedPeer (line 1455) | _evaluateJoinedPeer(peerId, roomType, roomId) {
method _onPairPeerJoined (line 1470) | _onPairPeerJoined(peerId, roomSecret) {
method _onPublicRoomJoinKeyInvalid (line 1495) | _onPublicRoomJoinKeyInvalid() {
method _close (line 1499) | _close() {
method _pairDeviceCancel (line 1503) | _pairDeviceCancel() {
method _onPairDeviceCanceled (line 1509) | _onPairDeviceCanceled(pairKey) {
method _cleanUp (line 1513) | _cleanUp() {
method _onSecretRoomDeleted (line 1520) | _onSecretRoomDeleted(roomSecret) {
method _evaluateNumberRoomSecrets (line 1528) | _evaluateNumberRoomSecrets() {
class EditPairedDevicesDialog (line 1545) | class EditPairedDevicesDialog extends Dialog {
method constructor (line 1546) | constructor() {
method _onKeyDown (line 1558) | _onKeyDown(e) {
method _initDOM (line 1566) | async _initDOM() {
method hide (line 1631) | hide() {
method _onEditPairedDevices (line 1638) | _onEditPairedDevices() {
method _clearRoomSecrets (line 1646) | _clearRoomSecrets() {
method _onPeerDisplayNameChanged (line 1661) | _onPeerDisplayNameChanged(e) {
class PublicRoomDialog (line 1679) | class PublicRoomDialog extends Dialog {
method constructor (line 1680) | constructor() {
method _onKeyDown (line 1721) | _onKeyDown(e) {
method _onPaste (line 1729) | _onPaste(e) {
method _onHeaderBtnClick (line 1735) | _onHeaderBtnClick() {
method _createPublicRoom (line 1744) | _createPublicRoom() {
method _onPublicRoomCreated (line 1748) | _onPublicRoomCreated(roomId) {
method _setKeyAndQrCode (line 1758) | _setKeyAndQrCode() {
method setFooterBadge (line 1779) | setFooterBadge() {
method _getShareRoomUrl (line 1790) | _getShareRoomUrl() {
method _copyShareRoomUrl (line 1796) | _copyShareRoomUrl() {
method _onWsConnected (line 1806) | _onWsConnected() {
method _onSubmit (line 1817) | _onSubmit(e) {
method _submit (line 1822) | _submit() {
method _joinPublicRoom (line 1827) | _joinPublicRoom(roomId, createIfInvalid = false) {
method _onPeers (line 1841) | _onPeers(message) {
method _onPeerJoined (line 1847) | _onPeerJoined(message) {
method _evaluateJoinedPeer (line 1851) | _evaluateJoinedPeer(peerId, roomId) {
method _onPublicRoomIdInvalid (line 1868) | _onPublicRoomIdInvalid(roomId) {
method _leavePublicRoom (line 1875) | _leavePublicRoom() {
method _onPublicRoomLeft (line 1879) | _onPublicRoomLeft() {
method show (line 1886) | show() {
method hide (line 1891) | hide() {
method _cleanUp (line 1896) | _cleanUp() {
class SendTextDialog (line 1905) | class SendTextDialog extends Dialog {
method constructor (line 1906) | constructor() {
method _onKeyDown (line 1922) | _onKeyDown(e) {
method _onDrop (line 1935) | async _onDrop(e) {
method _onPaste (line 1949) | async _onPaste(e) {
method _textEmpty (line 1969) | _textEmpty() {
method _onInput (line 1973) | _onInput() {
method _onRecipient (line 1985) | _onRecipient(peerId, deviceName) {
method _onSubmit (line 2001) | _onSubmit(e) {
method _send (line 2006) | _send() {
class ReceiveTextDialog (line 2016) | class ReceiveTextDialog extends Dialog {
method constructor (line 2017) | constructor() {
method selectionEmpty (line 2034) | selectionEmpty() {
method _onKeyDown (line 2038) | async _onKeyDown(e) {
method _onText (line 2049) | _onText(text, peerId) {
method _dequeueRequests (line 2060) | _dequeueRequests() {
method _showReceiveTextDialog (line 2068) | _showReceiveTextDialog(text, peerId) {
method _setDocumentTitleMessages (line 2147) | _setDocumentTitleMessages() {
method _onCopy (line 2153) | async _onCopy() {
method hide (line 2166) | hide() {
class ShareTextDialog (line 2182) | class ShareTextDialog extends Dialog {
method constructor (line 2183) | constructor() {
method isApproveShareTextSet (line 2205) | static isApproveShareTextSet() {
method _setCheckboxValueToLocalStorage (line 2209) | _setCheckboxValueToLocalStorage() {
method _onKeyDown (line 2213) | _onKeyDown(e) {
method _textEmpty (line 2226) | _textEmpty() {
method _evaluateEmptyText (line 2230) | _evaluateEmptyText() {
method _onShareText (line 2242) | _onShareText(text) {
method _approveShareText (line 2248) | _approveShareText() {
method hide (line 2253) | hide() {
class Base64Dialog (line 2259) | class Base64Dialog extends Dialog {
method constructor (line 2261) | constructor() {
method evaluateBase64Text (line 2269) | async evaluateBase64Text(base64Text, hash) {
method evaluateBase64Zip (line 2292) | async evaluateBase64Zip(base64Zip, hash) {
method _setPasteBtnToProcessing (line 2307) | _setPasteBtnToProcessing() {
method preparePasting (line 2312) | preparePasting(type) {
method processInput (line 2333) | async processInput(type) {
method processClipboard (line 2339) | async processClipboard(type) {
method processPastedBase64 (line 2344) | async processPastedBase64(type, base64) {
method processBase64Text (line 2360) | async processBase64Text(base64){
method processBase64Zip (line 2380) | async processBase64Zip(base64) {
method hide (line 2395) | hide() {
class AboutUI (line 2404) | class AboutUI {
method constructor (line 2405) | constructor() {
method _onConfig (line 2415) | async _onConfig(btnConfig) {
method _evaluateBtnConfig (line 2424) | async _evaluateBtnConfig($btn, config) {
class Toast (line 2451) | class Toast extends Dialog {
method constructor (line 2452) | constructor() {
method _onNotify (line 2462) | _onNotify(message) {
method hide (line 2472) | hide() {
class Notifications (line 2478) | class Notifications {
method constructor (line 2480) | constructor() {
method _requestPermission (line 2495) | async _requestPermission() {
method _notify (line 2507) | _notify(title, body) {
method _messageNotification (line 2533) | _messageNotification(message, peerId) {
method _downloadNotification (line 2547) | _downloadNotification(files) {
method _requestNotification (line 2574) | _requestNotification(request, peerId) {
method _download (line 2602) | _download(notification) {
method _copyText (line 2607) | async _copyText(message, notification) {
method _bind (line 2617) | _bind(notification, handler) {
class NetworkStatusUI (line 2633) | class NetworkStatusUI {
method constructor (line 2635) | constructor() {
method _showOfflineMessage (line 2641) | _showOfflineMessage() {
method _showOnlineMessage (line 2648) | _showOnlineMessage() {
class WebShareTargetUI (line 2653) | class WebShareTargetUI {
method evaluateShareTarget (line 2655) | async evaluateShareTarget(shareTargetType, title, text, url) {
class WebFileHandlersUI (line 2701) | class WebFileHandlersUI {
method evaluateLaunchQueue (line 2702) | async evaluateLaunchQueue() {
class NoSleepUI (line 2724) | class NoSleepUI {
method constructor (line 2725) | constructor() {
method enable (line 2729) | static enable() {
method disable (line 2736) | static disable() {
FILE: public/scripts/util.js
method createNewZipWriter (line 69) | createNewZipWriter() {
method addFile (line 72) | addFile(file, options) {
method getBlobURL (line 75) | async getBlobURL() {
method getZipFile (line 85) | async getZipFile(filename = "archive.zip") {
method getEntries (line 95) | async getEntries(file, options) {
method getData (line 98) | async getData(entry, options) {
method guessMimeByFilename (line 400) | guessMimeByFilename(filename) {
method addMissingMimeTypesToFiles (line 409) | addMissingMimeTypesToFiles(files) {
function onlyUnique (line 440) | function onlyUnique (value, index, array) {
function getUrlWithoutArguments (line 444) | function getUrlWithoutArguments() {
function changeFavicon (line 448) | function changeFavicon(src) {
function arrayBufferToBase64 (line 453) | function arrayBufferToBase64(buffer) {
function base64ToArrayBuffer (line 463) | function base64ToArrayBuffer(base64) {
function fileToBlob (line 473) | async function fileToBlob (file) {
function getThumbnailAsDataUrl (line 477) | function getThumbnailAsDataUrl(file, width = undefined, height = undefin...
function waitUntilImageIsLoaded (line 535) | function waitUntilImageIsLoaded(imageUrl, timeout = 10000) {
function decodeBase64Files (line 567) | async function decodeBase64Files(base64) {
function decodeBase64Text (line 586) | async function decodeBase64Text(base64) {
function isUrlValid (line 592) | function isUrlValid(url) {
FILE: public/scripts/worker/canvas-worker.js
function createCanvas (line 35) | function createCanvas(data) {
function initCanvas (line 51) | function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {
function startAnimation (line 70) | function startAnimation() {
function switchAnimation (line 75) | function switchAnimation(state) {
function onShareModeChange (line 84) | function onShareModeChange(active) {
function drawCircle (line 90) | function drawCircle(ctx, radius) {
function drawCircles (line 111) | function drawCircles(ctx, frame) {
function drawFrame (line 118) | function drawFrame(frame) {
function animateBg (line 123) | function animateBg() {
FILE: server/helper.js
method hashCodeSalted (line 6) | hashCodeSalted(salt) {
method getRandomString (line 25) | getRandomString(length, lettersOnly = false) {
FILE: server/peer.js
class Peer (line 6) | class Peer {
method constructor (line 8) | constructor(socket, request, conf) {
method rateLimitReached (line 34) | rateLimitReached() {
method _setIP (line 44) | _setIP(request) {
method ipIsPrivate (line 88) | ipIsPrivate(ip) {
method _setPeerId (line 128) | _setPeerId(request) {
method _setRtcSupported (line 139) | _setRtcSupported(request) {
method _setName (line 144) | _setName(req) {
method getInfo (line 181) | getInfo() {
method isValidUuid (line 189) | static isValidUuid(uuid) {
method isPeerIdHashValid (line 193) | isPeerIdHashValid(peerId, peerIdHash) {
method addRoomSecret (line 197) | addRoomSecret(roomSecret) {
method removeRoomSecret (line 203) | removeRoomSecret(roomSecret) {
FILE: server/server.js
class PairDropServer (line 7) | class PairDropServer {
method constructor (line 9) | constructor(conf) {
FILE: server/ws-server.js
class PairDropWsServer (line 7) | class PairDropWsServer {
method constructor (line 9) | constructor(server, conf) {
method _onConnection (line 21) | _onConnection(peer) {
method _onMessage (line 45) | _onMessage(sender, message) {
method _signalAndRelay (line 115) | _signalAndRelay(sender, message) {
method _onDisconnect (line 133) | _onDisconnect(sender) {
method _disconnect (line 137) | _disconnect(sender) {
method _onRoomSecrets (line 151) | _onRoomSecrets(sender, message) {
method _onRoomSecretsDeleted (line 163) | _onRoomSecretsDeleted(sender, message) {
method _deleteSecretRoom (line 169) | _deleteSecretRoom(roomSecret) {
method _onPairDeviceInitiate (line 185) | _onPairDeviceInitiate(sender) {
method _onPairDeviceJoin (line 202) | _onPairDeviceJoin(sender, message) {
method _onPairDeviceCancel (line 230) | _onPairDeviceCancel(sender) {
method _onCreatePublicRoom (line 242) | _onCreatePublicRoom(sender) {
method _onJoinPublicRoom (line 253) | _onJoinPublicRoom(sender, message) {
method _onLeavePublicRoom (line 268) | _onLeavePublicRoom(sender) {
method _onRegenerateRoomSecret (line 273) | _onRegenerateRoomSecret(sender, message) {
method _createPairKey (line 290) | _createPairKey(creator, roomSecret) {
method _removePairKey (line 305) | _removePairKey(pairKey) {
method _joinIpRoom (line 312) | _joinIpRoom(peer) {
method _joinSecretRoom (line 316) | _joinSecretRoom(peer, roomSecret) {
method _joinPublicRoom (line 323) | _joinPublicRoom(peer, publicRoomId) {
method _joinRoom (line 332) | _joinRoom(peer, roomType, roomId) {
method _leaveIpRoom (line 351) | _leaveIpRoom(peer, disconnect = false) {
method _leaveSecretRoom (line 355) | _leaveSecretRoom(peer, roomSecret, disconnect = false) {
method _leavePublicRoom (line 362) | _leavePublicRoom(peer, disconnect = false) {
method _leaveRoom (line 370) | _leaveRoom(peer, roomType, roomId, disconnect = false) {
method _notifyPeers (line 398) | _notifyPeers(peer, roomType, roomId) {
method _joinSecretRooms (line 433) | _joinSecretRooms(peer, roomSecrets) {
method _leaveAllSecretRooms (line 439) | _leaveAllSecretRooms(peer, disconnect = false) {
method _send (line 445) | _send(peer, message) {
method _keepAlive (line 452) | _keepAlive(peer) {
method _cancelKeepAlive (line 474) | _cancelKeepAlive(peer) {
method _setKeepAliveTimerToNow (line 480) | _setKeepAliveTimerToNow(peer) {
Condensed preview — 102 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (893K chars).
[
{
"path": ".dockerignore",
"chars": 133,
"preview": ".github\n.git*\n.idea\ndev\ndocs\nlicenses\nnode_modules\npairdrop-cli\n*.md\n*.yml\nDockerfile\nrtc_config_example.json\nturnserver"
},
{
"path": ".github/FUNDING.yml",
"chars": 610,
"preview": "# These are supported funding model platforms\n\ngithub: schlagmichdoch\npatreon: # Replace with a single Patreon username\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 1148,
"preview": "---\nname: Bug Report\nabout: Create a report to help us improve. Please check the FAQ first.\ntitle: '[Bug] '\nlabels: 'bug"
},
{
"path": ".github/ISSUE_TEMPLATE/enhancement.md",
"chars": 482,
"preview": "---\nname: Enhancement\nabout: Enhancements and feature requests are always welcome. See discussions regarding central top"
},
{
"path": ".github/dependabot.yml",
"chars": 502,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/docker-image.yml",
"chars": 784,
"preview": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n"
},
{
"path": ".github/workflows/github-image.yml",
"chars": 1998,
"preview": "# This workflow uses actions that are not certified by GitHub.\r\n# They are provided by a third-party and are governed by"
},
{
"path": ".github/workflows/zip-release.yml",
"chars": 1159,
"preview": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n"
},
{
"path": ".gitignore",
"chars": 83,
"preview": "node_modules\n.DS_Store\n/dev/certs\nqrcode-svg/\nturnserver.conf\nrtc_config.json\nssl/\n"
},
{
"path": ".npmrc",
"chars": 19,
"preview": "engine-strict=true\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 1744,
"preview": "# Priorities\n- PairDrop should be extremely simple, clean, and easy to use.\n- The main user flow should never be obstruc"
},
{
"path": "Dockerfile",
"chars": 441,
"preview": "FROM alpine:latest\n\nWORKDIR /home/node/app\n\nCOPY package*.json ./\n\nRUN apk add --no-cache nodejs npm\nRUN NODE_ENV=\"produ"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 7154,
"preview": "<div align=\"center\">\n <a href=\"https://github.com/schlagmichdoch/PairDrop\">\n <img src=\"public/images/android-chrome-"
},
{
"path": "dev/nginx/default.conf",
"chars": 908,
"preview": "server {\n listen 80;\n\n expires epoch;\n\n location / {\n proxy_connect_timeout 300;\n proxy_pas"
},
{
"path": "dev/nginx-with-openssl.Dockerfile",
"chars": 49,
"preview": "FROM nginx:alpine\n\nRUN apk add --no-cache openssl"
},
{
"path": "dev/openssl/create.sh",
"chars": 552,
"preview": "#!/bin/sh\n\ncnf_dir='/mnt/openssl/'\ncerts_dir='/etc/ssl/certs/'\nopenssl req -config ${cnf_dir}pairdropCA.cnf -new -x509 -"
},
{
"path": "dev/openssl/pairdropCA.cnf",
"chars": 731,
"preview": "[ req ]\ndefault_bits = 2048\ndefault_md = sha256\ndefault_days = 1\nencrypt_key = no\ndistinguish"
},
{
"path": "dev/openssl/pairdropCert.cnf",
"chars": 996,
"preview": "[ req ]\ndefault_bits = 2048\ndefault_md = sha256\ndefault_days = 1\nencrypt_key = no\ndisting"
},
{
"path": "docker-compose-coturn.yml",
"chars": 1288,
"preview": "version: \"3\"\nservices:\n pairdrop:\n image: \"lscr.io/linuxserver/pairdrop:latest\"\n container_name: pairdrop\n res"
},
{
"path": "docker-compose-dev.yml",
"chars": 1274,
"preview": "version: \"3\"\nservices:\n pairdrop:\n build: .\n container_name: pairdrop\n restart: unless-stopped\n environment"
},
{
"path": "docker-compose.yml",
"chars": 806,
"preview": "version: \"3\"\nservices:\n pairdrop:\n image: \"lscr.io/linuxserver/pairdrop:latest\"\n container_name: pairdrop\n res"
},
{
"path": "docs/docker-swarm-usage.md",
"chars": 1592,
"preview": "# Docker Swarm Usage\n\n## Healthcheck\n\nThe [Docker Image](../Dockerfile) includes a health check with the following optio"
},
{
"path": "docs/faq.md",
"chars": 9960,
"preview": "# Frequently Asked Questions\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-wei"
},
{
"path": "docs/host-your-own.md",
"chars": 20687,
"preview": "# Deployment Notes\n\n## TURN server for Internet Transfer\n\nBeware that you have to host your own TURN server to enable tr"
},
{
"path": "docs/how-to.md",
"chars": 5723,
"preview": "# How-To\n\n## Send directly from share menu on iOS\nI created an iOS shortcut to send images, files, folder, URLs \\\nor tex"
},
{
"path": "docs/technical-documentation.md",
"chars": 3285,
"preview": "# Technical Documentation\n## Encryption, WebRTC, STUN and TURN\n\nEncryption is mandatory for WebRTC connections and compl"
},
{
"path": "licenses/BSD_3-Clause-zip-js",
"chars": 1500,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2023, Gildas Lormeau\n\nRedistribution and use in source and binary forms, with or wit"
},
{
"path": "licenses/MIT-NoSleep",
"chars": 1073,
"preview": "The MIT License (MIT)\n\nCopyright (c) Rich Tibbett\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "licenses/MIT-heic2any",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2020 Alex Corvi\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy"
},
{
"path": "package.json",
"chars": 497,
"preview": "{\n \"name\": \"pairdrop\",\n \"version\": \"1.11.2\",\n \"type\": \"module\",\n \"description\": \"\",\n \"main\": \"server/index.js\",\n \""
},
{
"path": "pairdrop-cli/.gitignore",
"chars": 20,
"preview": ".pairdrop-cli-config"
},
{
"path": "pairdrop-cli/.pairdrop-cli-config.example",
"chars": 28,
"preview": "DOMAIN=https://pairdrop.net/"
},
{
"path": "pairdrop-cli/pairdrop",
"chars": 11625,
"preview": "#!/bin/bash\nset -e\n\n# PairDrop version when this file was last changed\nversion=\"v1.10.4\"\n\n##############################"
},
{
"path": "pairdrop-cli/pairdrop.sh",
"chars": 125,
"preview": "#!/bin/bash\nparent_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" || exit ; pwd -P )\n\ncd \"$parent_path\" || exit\n\n./pairdrop"
},
{
"path": "pairdrop-cli/send-with-pairdrop",
"chars": 297,
"preview": "#!/bin/bash\n\n# Initialize an array\nlines=()\n\n# Read each line into the array\nwhile IFS= read -r line; do\n lines+=(\"$l"
},
{
"path": "pairdrop-cli/send-with-pairdrop.ps1",
"chars": 94,
"preview": "$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\n& \"$scriptDir\\pairdrop.sh\" $args"
},
{
"path": "public/fonts/OpenSans/OFL.txt",
"chars": 4483,
"preview": "Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)\r\n\r\nThis Font Software is licensed"
},
{
"path": "public/fonts/OpenSans/README.txt",
"chars": 3514,
"preview": "Open Sans Variable Font\n=======================\n\nThis download contains Open Sans as both variable fonts and static font"
},
{
"path": "public/index.html",
"chars": 58785,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/ht"
},
{
"path": "public/lang/ar.json",
"chars": 9741,
"preview": "{\n \"footer\": {\n \"webrtc\": \"إذا لم يكن WebRTC متاحًا.\",\n \"public-room-devices_title\": \"يمكن اكتشافك بواس"
},
{
"path": "public/lang/be.json",
"chars": 10771,
"preview": "{\n \"header\": {\n \"about_aria-label\": \"Адкрыйце Аб PairDrop\",\n \"about_title\": \"Аб PairDrop\",\n \"the"
},
{
"path": "public/lang/bg.json",
"chars": 10873,
"preview": "{\n \"header\": {\n \"about_title\": \"Относно PairDrop\",\n \"language-selector_title\": \"Задай език\",\n \"a"
},
{
"path": "public/lang/bn.json",
"chars": 1918,
"preview": "{\n \"header\": {\n \"about_title\": \"পেয়ার ড্রপ সম্পর্কে\",\n \"install_title\": \"পেয়ার ড্রপ ইন্সটল করুন\",\n "
},
{
"path": "public/lang/ca.json",
"chars": 10980,
"preview": "{\n \"instructions\": {\n \"x-instructions_mobile\": \"Toca per enviar fitxers o mantén premut per enviar un missatge"
},
{
"path": "public/lang/cs.json",
"chars": 10523,
"preview": "{\n \"header\": {\n \"about_aria-label\": \"Otevřít o PairDrop\",\n \"about_title\": \"O službě PairDrop\",\n "
},
{
"path": "public/lang/da.json",
"chars": 10317,
"preview": "{\n \"notifications\": {\n \"public-room-left\": \"Forlod det offentlige rum {{publicRoomId}}\",\n \"room-url-cop"
},
{
"path": "public/lang/de.json",
"chars": 11150,
"preview": "{\n \"header\": {\n \"about_title\": \"Über PairDrop\",\n \"notification_title\": \"Benachrichtigungen aktivieren\","
},
{
"path": "public/lang/en.json",
"chars": 10028,
"preview": "{\n \"header\": {\n \"about_title\": \"About PairDrop\",\n \"language-selector_title\": \"Set Language\",\n \"a"
},
{
"path": "public/lang/es.json",
"chars": 11139,
"preview": "{\n \"header\": {\n \"theme-auto_title\": \"Adaptar tema al sistema\",\n \"language-selector_title\": \"Configurar "
},
{
"path": "public/lang/et.json",
"chars": 10190,
"preview": "{\n \"notifications\": {\n \"rate-limit-join-key\": \"Jõudsid tegevuspiiranguni. Oota 10 sekundit ja proovi uuesti.\","
},
{
"path": "public/lang/eu.json",
"chars": 10542,
"preview": "{\n \"header\": {\n \"about_title\": \"PairDropi buruz\",\n \"about_aria-label\": \"Ireki PairDropi buruz\",\n "
},
{
"path": "public/lang/fa.json",
"chars": 10428,
"preview": "{\n \"header\": {\n \"theme-light_title\": \"همیشه از پوسته روشن استفاده شود\",\n \"theme-dark_title\": \"همیشه از "
},
{
"path": "public/lang/fi.json",
"chars": 1109,
"preview": "{\n \"header\": {\n \"about_title\": \"Tietoja PairDropista\",\n \"language-selector_title\": \"Valitse kieli\",\n "
},
{
"path": "public/lang/fr.json",
"chars": 11149,
"preview": "{\n \"header\": {\n \"about_title\": \"À propos de PairDrop\",\n \"language-selector_title\": \"Choix de la langue\""
},
{
"path": "public/lang/he.json",
"chars": 9382,
"preview": "{\n \"header\": {\n \"about_title\": \"אודות PairDrop\",\n \"theme-light_title\": \"השתמש תמיד במצב בהיר\",\n "
},
{
"path": "public/lang/hu.json",
"chars": 10920,
"preview": "{\n \"header\": {\n \"language-selector_title\": \"Nyelv beállítása\",\n \"theme-auto_title\": \"Téma automatikusan"
},
{
"path": "public/lang/id.json",
"chars": 10637,
"preview": "{\n \"footer\": {\n \"webrtc\": \"jika WebRTC tidak tersedia.\",\n \"public-room-devices_title\": \"Anda dapat dite"
},
{
"path": "public/lang/it.json",
"chars": 10976,
"preview": "{\n \"footer\": {\n \"webrtc\": \"se WebRTC non è disponibile.\",\n \"public-room-devices_title\": \"Puoi essere ri"
},
{
"path": "public/lang/ja.json",
"chars": 8391,
"preview": "{\n \"footer\": {\n \"webrtc\": \"(WebRTCが無効なため)\",\n \"public-room-devices_title\": \"公開ルーム内のデバイスは、別のネットワークからもアクセス"
},
{
"path": "public/lang/kab.json",
"chars": 1160,
"preview": "{\n \"dialogs\": {\n \"message_placeholder\": \"Aḍris\",\n \"send-message-title\": \"Azen izen\",\n \"copy\": \"N"
},
{
"path": "public/lang/kn.json",
"chars": 11568,
"preview": "{\n \"header\": {\n \"about_title\": \"PairDrop ಕುರಿತು\",\n \"cancel-paste-mode\": \"ಮಾಡಿದ\",\n \"theme-auto_ti"
},
{
"path": "public/lang/ko.json",
"chars": 8318,
"preview": "{\n \"header\": {\n \"notification_title\": \"알림 켜기\",\n \"edit-paired-devices_title\": \"연결된 기기 편집하기\",\n \"in"
},
{
"path": "public/lang/nb.json",
"chars": 10435,
"preview": "{\n \"header\": {\n \"edit-paired-devices_title\": \"Rediger sammenkoblede enheter\",\n \"about_title\": \"Om PairD"
},
{
"path": "public/lang/nl.json",
"chars": 10812,
"preview": "{\n \"footer\": {\n \"webrtc\": \"als WebRTC niet beschikbaar is.\",\n \"public-room-devices_title\": \"U kan door "
},
{
"path": "public/lang/nn.json",
"chars": 10252,
"preview": "{\n \"header\": {\n \"language-selector_title\": \"Språk\",\n \"theme-light_title\": \"Bruk alltid lyst tema\",\n "
},
{
"path": "public/lang/pl.json",
"chars": 10806,
"preview": "{\n \"header\": {\n \"language-selector_title\": \"Ustaw język\",\n \"about_aria-label\": \"Otwórz \\\"O PairDrop\\\"\","
},
{
"path": "public/lang/pt-BR.json",
"chars": 11096,
"preview": "{\n \"header\": {\n \"about_title\": \"Sobre o PairDrop\",\n \"language-selector_title\": \"Definir idioma\",\n "
},
{
"path": "public/lang/ro.json",
"chars": 10994,
"preview": "{\n \"footer\": {\n \"webrtc\": \"dacă WebRTC nu este disponibil.\",\n \"public-room-devices_title\": \"Poți fi des"
},
{
"path": "public/lang/ru.json",
"chars": 11049,
"preview": "{\n \"header\": {\n \"about_aria-label\": \"О PairDrop\",\n \"pair-device_title\": \"Связать ваши устройства навсег"
},
{
"path": "public/lang/sk.json",
"chars": 10454,
"preview": "{\n \"header\": {\n \"install_title\": \"Nainštalovať PairDrop\",\n \"about_title\": \"O službe PairDrop\",\n "
},
{
"path": "public/lang/ta.json",
"chars": 11149,
"preview": "{\n \"header\": {\n \"language-selector_title\": \"மொழியை அமைக்கவும்\",\n \"about_aria-label\": \"இணை ட்ராப் பற்றி "
},
{
"path": "public/lang/th.json",
"chars": 4704,
"preview": "{\n \"footer\": {\n \"display-name_title\": \"แก้ไขชื่ออุปกรณ์ของคุณอย่างถาวร\",\n \"on-this-network_title\": \"ทุก"
},
{
"path": "public/lang/tr.json",
"chars": 10552,
"preview": "{\n \"header\": {\n \"about_title\": \"PairDrop Hakkında\",\n \"about_aria-label\": \"PairDrop Hakkında'yı Aç\",\n "
},
{
"path": "public/lang/uk.json",
"chars": 10834,
"preview": "{\n \"header\": {\n \"about_aria-label\": \"Відкрити \\\"Про PairDrop\\\"\",\n \"theme-auto_title\": \"Автоматично адап"
},
{
"path": "public/lang/zh-CN.json",
"chars": 7613,
"preview": "{\n \"header\": {\n \"about_title\": \"关于 PairDrop\",\n \"about_aria-label\": \"打开 关于 PairDrop\",\n \"theme-lig"
},
{
"path": "public/lang/zh-HK.json",
"chars": 7462,
"preview": "{\n \"header\": {\n \"expand_title\": \"展開標題按鈕列\",\n \"about_title\": \"關於 PairDrop\",\n \"language-selector_ti"
},
{
"path": "public/lang/zh-TW.json",
"chars": 7578,
"preview": "{\n \"header\": {\n \"language-selector_title\": \"設定語言\",\n \"theme-auto_title\": \"自動使用系統主題\",\n \"cancel-sha"
},
{
"path": "public/manifest.json",
"chars": 2480,
"preview": "{\n \"name\": \"PairDrop\",\n \"short_name\": \"PairDrop\",\n \"icons\": [\n {\n \"src\": \"images/android-chro"
},
{
"path": "public/robots.txt",
"chars": 24,
"preview": "User-agent: *\nDisallow:\n"
},
{
"path": "public/scripts/browser-tabs-connector.js",
"chars": 2071,
"preview": "class BrowserTabsConnector {\n constructor() {\n if (!('BroadcastChannel' in window)) return;\n\n this.bc ="
},
{
"path": "public/scripts/localization.js",
"chars": 7945,
"preview": "class Localization {\n constructor() {\n Localization.$htmlRoot = document.querySelector('html');\n\n Local"
},
{
"path": "public/scripts/main.js",
"chars": 8878,
"preview": "class PairDrop {\n\n constructor() {\n this.$headerNotificationBtn = $('notification');\n this.$headerEditP"
},
{
"path": "public/scripts/network.js",
"chars": 44068,
"preview": "class ServerConnection {\n\n constructor() {\n Events.on('pagehide', _ => this._disconnect());\n Events.on("
},
{
"path": "public/scripts/persistent-storage.js",
"chars": 13427,
"preview": "class PersistentStorage {\n constructor() {\n if (!('indexedDB' in window)) {\n PersistentStorage.logB"
},
{
"path": "public/scripts/ui-main.js",
"chars": 19128,
"preview": "// Selector shortcuts\nconst $ = query => document.getElementById(query);\nconst $$ = query => document.querySelector(quer"
},
{
"path": "public/scripts/ui.js",
"chars": 96978,
"preview": "class PeersUI {\n\n constructor() {\n this.$xPeers = $$('x-peers');\n this.$xNoPeers = $$('x-no-peers');\n "
},
{
"path": "public/scripts/util.js",
"chars": 20609,
"preview": "// Polyfill for Navigator.clipboard.writeText\nif (!navigator.clipboard) {\n navigator.clipboard = {\n writeText:"
},
{
"path": "public/scripts/worker/canvas-worker.js",
"chars": 3737,
"preview": "self.onmessage = (e) => {\n switch (e.data.type) {\n case \"createCanvas\": createCanvas(e.data);\n brea"
},
{
"path": "public/service-worker.js",
"chars": 8654,
"preview": "const cacheVersion = 'v1.11.2';\nconst cacheTitle = `pairdrop-cache-${cacheVersion}`;\nconst relativePathsToCache = [\n "
},
{
"path": "public/styles/styles-deferred.css",
"chars": 15372,
"preview": "/* All styles in this sheet are not needed on page load and deferred */\n\n/* Text Input */\n.textarea {\n box-sizing: bo"
},
{
"path": "public/styles/styles-main.css",
"chars": 21500,
"preview": "/* All styles in this sheet are needed on page load */\n\n/* Layout */\n\nhtml,\nbody {\n margin: 0;\n display: flex;\n "
},
{
"path": "rtc_config_example.json",
"chars": 218,
"preview": "{\n \"sdpSemantics\": \"unified-plan\",\n \"iceServers\": [\n {\n \"urls\": \"stun:<DOMAIN>:3478\"\n },\n {\n \"urls\""
},
{
"path": "server/helper.js",
"chars": 2444,
"preview": "import crypto from \"crypto\";\n\nexport const hasher = (() => {\n let password;\n return {\n hashCodeSalted(salt)"
},
{
"path": "server/index.js",
"chars": 5887,
"preview": "import {spawn} from \"child_process\";\nimport fs from \"fs\";\n\nimport PairDropServer from \"./server.js\";\nimport PairDropWsSe"
},
{
"path": "server/peer.js",
"chars": 6427,
"preview": "import crypto from \"crypto\";\nimport parser from \"ua-parser-js\";\nimport {animals, colors, uniqueNamesGenerator} from \"uni"
},
{
"path": "server/server.js",
"chars": 3089,
"preview": "import express from \"express\";\nimport RateLimit from \"express-rate-limit\";\nimport {fileURLToPath} from \"url\";\nimport pat"
},
{
"path": "server/ws-server.js",
"chars": 14458,
"preview": "import {WebSocketServer} from \"ws\";\nimport crypto from \"crypto\"\n\nimport Peer from \"./peer.js\";\nimport {hasher, randomize"
},
{
"path": "turnserver_example.conf",
"chars": 1259,
"preview": "# TURN server name and realm\nrealm=<DOMAIN>\nserver-name=pairdrop\n\n# IPs the TURN server listens to\nlistening-ip=0.0.0.0\n"
}
]
// ... and 3 more files (download for full content)
About this extraction
This page contains the full source code of the schlagmichdoch/PairDrop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 102 files (819.4 KB), approximately 222.1k tokens, and a symbol index with 530 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.