Merge branch 'next' into translate

This commit is contained in:
schlagmichdoch 2023-11-23 19:46:02 +01:00
commit d78c138dad
136 changed files with 5042 additions and 13894 deletions

View file

@ -0,0 +1,93 @@
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.

View file

@ -0,0 +1,100 @@
Open Sans Variable Font
=======================
This download contains Open Sans as both variable fonts and static fonts.
Open Sans is a variable font with these axes:
wdth
wght
This means all the styles are contained in these files:
OpenSans-VariableFont_wdth,wght.ttf
OpenSans-Italic-VariableFont_wdth,wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Open Sans:
static/OpenSans_Condensed-Light.ttf
static/OpenSans_Condensed-Regular.ttf
static/OpenSans_Condensed-Medium.ttf
static/OpenSans_Condensed-SemiBold.ttf
static/OpenSans_Condensed-Bold.ttf
static/OpenSans_Condensed-ExtraBold.ttf
static/OpenSans_SemiCondensed-Light.ttf
static/OpenSans_SemiCondensed-Regular.ttf
static/OpenSans_SemiCondensed-Medium.ttf
static/OpenSans_SemiCondensed-SemiBold.ttf
static/OpenSans_SemiCondensed-Bold.ttf
static/OpenSans_SemiCondensed-ExtraBold.ttf
static/OpenSans-Light.ttf
static/OpenSans-Regular.ttf
static/OpenSans-Medium.ttf
static/OpenSans-SemiBold.ttf
static/OpenSans-Bold.ttf
static/OpenSans-ExtraBold.ttf
static/OpenSans_Condensed-LightItalic.ttf
static/OpenSans_Condensed-Italic.ttf
static/OpenSans_Condensed-MediumItalic.ttf
static/OpenSans_Condensed-SemiBoldItalic.ttf
static/OpenSans_Condensed-BoldItalic.ttf
static/OpenSans_Condensed-ExtraBoldItalic.ttf
static/OpenSans_SemiCondensed-LightItalic.ttf
static/OpenSans_SemiCondensed-Italic.ttf
static/OpenSans_SemiCondensed-MediumItalic.ttf
static/OpenSans_SemiCondensed-SemiBoldItalic.ttf
static/OpenSans_SemiCondensed-BoldItalic.ttf
static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
static/OpenSans-LightItalic.ttf
static/OpenSans-Italic.ttf
static/OpenSans-MediumItalic.ttf
static/OpenSans-SemiBoldItalic.ttf
static/OpenSans-BoldItalic.ttf
static/OpenSans-ExtraBoldItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View file

@ -5,17 +5,17 @@
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop</title>
<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="no">
<meta name="apple-mobile-web-app-capable" content="yes">
<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="RobinLinus">
<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/">
@ -28,13 +28,14 @@
<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="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="styles/styles-main.css">
<link rel="manifest" href="manifest.json">
</head>
@ -94,7 +95,7 @@
<use xlink:href="#public-room-icon"></use>
</svg>
</div>
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden></div>
<div id="cancel-paste-mode" class="btn" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden></div>
</header>
<!-- Center -->
<div id="center" class="opacity-0">
@ -108,11 +109,22 @@
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg">
<p id="paste-filename"></p>
</x-instructions>
<div id="websocket-fallback" 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">
<use xlink:href="#wifi-tethering"></use>
<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">
@ -127,9 +139,9 @@
<span data-i18n-key="footer.discovery" data-i18n-attrs="text"></span>
</div>
<div class="row center">
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
<span class="badge badge-gradient badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title"></span>
<span class="badge badge-gradient badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden></span>
<span class="badge badge-gradient badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
</div>
</div>
</div>
@ -142,83 +154,83 @@
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text"></h2>
</div>
<div class="language-buttons">
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></button>
<button class="button fw" value="ar">
<button class="btn fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text"></button>
<button class="btn fw" value="ar">
<span>العربية</span>
<span>-</span>
<span>(Arabic)</span>
</button>
<button class="button fw" value="de">
<button class="btn fw" value="de">
<span>Deutsch</span>
<span>-</span>
<span>(German)</span>
</button>
<button class="button fw" value="en">
<button class="btn fw" value="en">
<span>English</span>
</button>
<button class="button fw" value="es">
<button class="btn fw" value="es">
<span>Español</span>
<span>-</span>
<span>(Spanish)</span>
</button>
<button class="button fw" value="fr">
<button class="btn fw" value="fr">
<span>Français</span>
<span>-</span>
<span>(French)</span>
</button>
<button class="button fw" value="id">
<button class="btn fw" value="id">
<span>Bahasa Indonesia</span>
<span>-</span>
<span>(Indonesian)</span>
</button>
<button class="button fw" value="it">
<button class="btn fw" value="it">
<span>Italiano</span>
<span>-</span>
<span>(Italian)</span>
</button>
<button class="button fw" value="nl">
<button class="btn fw" value="nl">
<span>Nederlands</span>
<span>-</span>
<span>(Dutch)</span>
</button>
<button class="button fw" value="nb">
<button class="btn fw" value="nb">
<span>Norsk</span>
<span>-</span>
<span>(Norwegian)</span>
</button>
<button class="button fw" value="pt-BR">
<button class="btn fw" value="pt-BR">
<span>Português do Brasil</span>
<span>-</span>
<span>(Brazilian Portuguese)</span>
</button>
<button class="button fw" value="ro">
<button class="btn fw" value="ro">
<span>Română</span>
<span>-</span>
<span>(Romanian)</span>
</button>
<button class="button fw" value="ru">
<button class="btn fw" value="ru">
<span>Русский язык</span>
<span>-</span>
<span>(Russian)</span>
</button>
<button class="button fw" value="tr">
<button class="btn fw" value="tr">
<span>Türkçe</span>
<span>-</span>
<span>(Turkish)</span>
</button>
<button class="button fw" value="zh-CN">
<button class="btn fw" value="zh-CN">
<span>中文</span>
<span>-</span>
<span>(Chinese)</span>
</button>
<button class="button fw" value="ja">
<button class="btn fw" value="ja">
<span>日本語</span>
<span>-</span>
<span>(Japanese)</span>
</button>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<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>
@ -248,7 +260,7 @@
</div>
</div>
<div class="row center">
<div class="column">
<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>
@ -261,8 +273,8 @@
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
<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>
@ -285,7 +297,7 @@
</p>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<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>
@ -318,7 +330,7 @@
</div>
</div>
<div class="row center">
<div class="column">
<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>
@ -330,9 +342,9 @@
</div>
</div>
<div class="center row-reverse button-row">
<button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button>
<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" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="btn btn-rounded btn-grey leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@ -347,10 +359,10 @@
<h2 class="center"></h2>
</div>
</div>
<div class="row center">
<div class="row center p1">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span class="display-name badge badge-gradient"></span>
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
@ -364,8 +376,8 @@
</div>
<div class="center file-preview"></div>
<div class="row-reverse center button-row">
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus></button>
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
<button id="accept-request" class="btn btn-rounded btn-grey" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus></button>
<button id="decline-request" class="btn btn-rounded btn-grey" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text"></button>
</div>
</x-paper>
</x-background>
@ -379,10 +391,10 @@
<h2 class="center"></h2>
</div>
</div>
<div class="row center">
<div class="row center p1">
<div class="column center file-description">
<div>
<span class="display-name badge"></span>
<span class="display-name badge badge-gradient"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
<div class="row file-name">
@ -396,9 +408,9 @@
</div>
<div class="center file-preview"></div>
<div class="row-reverse center button-row">
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus></button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<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></button>
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@ -413,22 +425,22 @@
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text"></h2>
</div>
</div>
<div class="row center display-name-wrapper">
<div class="row center p1 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>
<span class="display-name badge badge-gradient"></span>
</div>
</div>
</div>
<div class="row">
<div class="row p1">
<div class="column fw">
<div id="text-input" class="textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div id="text-input" class="fw textarea" role="textbox" data-i18n-key="dialogs.message" data-i18n-attrs="title placeholder" autofocus contenteditable></div>
</div>
</div>
<div class="button-row row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled></button>
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close></button>
<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>
@ -441,20 +453,20 @@
<div class="row center">
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text"></h2>
</div>
<div class="row center">
<div class="row center p1 display-name-wrapper">
<div class="text-center">
<span class="display-name badge"></span>
<span class="display-name badge badge-gradient"></span>
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text"></span>
</div>
</div>
<div class="row center">
<div class="row center p1">
<div class="column fw">
<div id="text" class="textarea fw"></div>
<div id="text" class="textarea"></div>
</div>
</div>
<div class="row-reverse center button-row">
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text"></button>
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text"></button>
<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>
@ -463,10 +475,10 @@
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste"></button>
<button class="btn btn-rounded btn-grey center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<div class="row-reverse center button-row">
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
</x-background>
@ -595,13 +607,9 @@
</svg>
<!-- Scripts -->
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script>
<script src="scripts/util.js"></script>
<script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script>
<script src="scripts/persistent-storage.js"></script>
<script src="scripts/ui-main.js"></script>
<script src="scripts/main.js"></script>
<!-- Sounds -->
<audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg">

View file

@ -25,7 +25,8 @@
"tap-to-send": "Tap to send",
"activate-paste-mode-base": "Open PairDrop on other devices to send",
"activate-paste-mode-and-other-files": "and {{count}} other files",
"activate-paste-mode-shared-text": "shared text"
"activate-paste-mode-shared-text": "shared text",
"webrtc-requirement": "To use PairDrop on this instance, WebRTC must be enabled!"
},
"footer": {
"known-as": "You are known as:",
@ -69,8 +70,9 @@
"share": "Share",
"download": "Download",
"send-message-title": "Send Message",
"send-message-to": "Send a Message to",
"send-message-to": "To:",
"message_title": "Insert message to send",
"message_placeholder": "Text",
"send": "Send",
"receive-text-title": "Message Received",
"copy": "Copy",

View file

@ -1,29 +1,30 @@
{
"name": "PairDrop",
"short_name": "PairDrop",
"icons": [{
"icons": [
{
"src": "images/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},{
},
{
"src": "images/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},{
},
{
"src": "images/android-chrome-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},{
},
{
"src": "images/android-chrome-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},{
"src": "images/favicon-96x96.png",
"sizes": "96x96",
"type": "image/png"
}],
}
],
"background_color": "#efefef",
"start_url": "/",
"scope": "/",

View file

@ -0,0 +1,60 @@
class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
}
_broadcastSend(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast:', e.data)
switch (e.data.type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
break;
}
}
static peerIsSameBrowser(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peer_ids_browser"));
return peerIdsBrowser
? peerIdsBrowser.indexOf(peerId) !== -1
: false;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem("peer_id");
if (!peerId) return false;
let peerIdsBrowser = [];
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peer_ids_browser"));
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
peerIdsBrowser.push(peerId);
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
static async removePeerIdFromLocalStorage(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peer_ids_browser"));
const index = peerIdsBrowser.indexOf(peerId);
peerIdsBrowser.splice(index, 1);
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static async removeOtherPeerIdsFromLocalStorage() {
const peerId = sessionStorage.getItem("peer_id");
if (!peerId) return false;
let peerIdsBrowser = [peerId];
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
}

View file

@ -15,7 +15,8 @@ class Localization {
? storedLanguageCode
: Localization.systemLocale;
Localization.setTranslation(Localization.initialLocale)
Localization
.setTranslation(Localization.initialLocale)
.then(_ => {
console.log("Initial translation successful.");
Events.fire("initial-translation-loaded");
@ -50,7 +51,8 @@ class Localization {
if (Localization.isRTLLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
} else {
}
else {
htmlRootNode.removeAttribute('dir');
}
@ -112,13 +114,9 @@ class Localization {
let attr = attrs[i];
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
} else {
if (attr.startsWith("data-")) {
let dataAttr = attr.substring(5);
element.dataset.dataAttr = Localization.getTranslation(key, attr);
} {
element.setAttribute(attr, Localization.getTranslation(key, attr));
}
}
else {
element.setAttribute(attr, Localization.getTranslation(key, attr));
}
}
}
@ -156,7 +154,8 @@ class Localization {
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
} else {
}
else {
console.warn("Missing translation in default language:", key, attr);
}
}

180
public/scripts/main.js Normal file
View file

@ -0,0 +1,180 @@
class PairDrop {
constructor() {
this.$header = $$('header.opacity-0');
this.$center = $$('#center');
this.$footer = $$('footer');
this.$xNoPeers = $$('x-no-peers');
this.$headerNotificationButton = $('notification');
this.$editPairedDevicesHeaderBtn = $('edit-paired-devices');
this.$footerInstructionsPairedDevices = $$('.discovery-wrapper .badge-room-secret');
this.$head = $$('head');
this.$installBtn = $('install');
this.registerServiceWorker();
Events.on('beforeinstallprompt', e => this.onPwaInstallable(e));
const persistentStorage = new PersistentStorage();
const themeUI = new ThemeUI();
const backgroundCanvas = new BackgroundCanvas();
Events.on('initial-translation-loaded', _ => {
// FooterUI needs translations
const footerUI = new FooterUI();
Events.on('fade-in-ui', _ => this.fadeInUI())
Events.on('fade-in-header', _ => this.fadeInHeader())
// Evaluate UI elements and fade in UI
this.evaluateUI();
// Load deferred assets
this.loadDeferredAssets();
});
// Translate page -> fires 'initial-translation-loaded' on finish
const localization = new Localization();
}
registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js')
.then(serviceWorker => {
console.log('Service Worker registered');
window.serviceWorker = serviceWorker
});
}
}
onPwaInstallable(e) {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
// only display install btn when not installed
this.$installBtn.removeAttribute('hidden');
this.$installBtn.addEventListener('click', () => {
this.$installBtn.setAttribute('hidden', true);
e.prompt();
});
}
return e.preventDefault();
}
evaluateUI() {
// Check whether notification permissions have already been granted
if ('Notification' in window && Notification.permission !== 'granted') {
this.$headerNotificationButton.removeAttribute('hidden');
}
PersistentStorage
.getAllRoomSecrets()
.then(roomSecrets => {
if (roomSecrets.length > 0) {
this.$editPairedDevicesHeaderBtn.removeAttribute('hidden');
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
}
})
.finally(() => {
Events.fire('evaluate-footer-badges');
Events.fire('fade-in-header');
});
}
fadeInUI() {
this.$center.classList.remove('opacity-0');
this.$footer.classList.remove('opacity-0');
// Prevent flickering on load
setTimeout(() => {
this.$xNoPeers.classList.remove('no-animation-on-load');
}, 600);
}
fadeInHeader() {
this.$header.classList.remove('opacity-0');
}
loadDeferredAssets() {
console.log("Load deferred assets");
this.deferredStyles = [
"styles/deferred-styles.css"
];
this.deferredScripts = [
"scripts/browser-tabs-connector.js",
"scripts/util.js",
"scripts/network.js",
"scripts/ui.js",
"scripts/qr-code.min.js",
"scripts/zip.min.js",
"scripts/no-sleep.min.js"
];
this.deferredStyles.forEach(url => this.loadStyleSheet(url, _ => this.onStyleLoaded(url)))
this.deferredScripts.forEach(url => this.loadScript(url, _ => this.onScriptLoaded(url)))
}
loadStyleSheet(url, callback) {
let stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = url;
stylesheet.type = 'text/css';
stylesheet.onload = callback;
this.$head.appendChild(stylesheet);
}
loadScript(url, callback) {
let script = document.createElement("script");
script.src = url;
script.onload = callback;
document.body.appendChild(script);
}
onStyleLoaded(url) {
// remove entry from array
const index = this.deferredStyles.indexOf(url);
if (index !== -1) {
this.deferredStyles.splice(index, 1);
}
this.onAssetLoaded();
}
onScriptLoaded(url) {
// remove entry from array
const index = this.deferredScripts.indexOf(url);
if (index !== -1) {
this.deferredScripts.splice(index, 1);
}
this.onAssetLoaded();
}
onAssetLoaded() {
if (this.deferredScripts.length || this.deferredStyles.length) return;
console.log("Loading of deferred assets completed. Start UI hydration.");
this.hydrate();
}
hydrate() {
const peersUI = new PeersUI();
const languageSelectDialog = new LanguageSelectDialog();
const receiveFileDialog = new ReceiveFileDialog();
const receiveRequestDialog = new ReceiveRequestDialog();
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const pairDeviceDialog = new PairDeviceDialog();
const clearDevicesDialog = new EditPairedDevicesDialog();
const publicRoomDialog = new PublicRoomDialog();
const base64ZipDialog = new Base64ZipDialog();
const toast = new Toast();
const notifications = new Notifications();
const networkStatusUI = new NetworkStatusUI();
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new BrowserTabsConnector();
const server = new ServerConnection();
const peers = new PeersManager(server);
console.log("UI hydrated.")
}
}
const pairDrop = new PairDrop();

View file

@ -1,26 +1,15 @@
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) {
navigator.connection.addEventListener('change', _ => this._reconnect());
}
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('join-ip-room', _ => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
@ -33,6 +22,49 @@ class ServerConnection {
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
Events.on('online', _ => this._connect());
this._getConfig().then(() => this._connect());
}
_getConfig() {
console.log("Loading config...")
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
// Config received
let config = JSON.parse(xhr.responseText);
console.log("Config loaded:", config)
this._config = config;
Events.fire('config', config);
resolve()
} else if (xhr.status < 200 || xhr.status >= 300) {
retry(xhr);
}
})
xhr.addEventListener("error", _ => {
retry(xhr);
});
function retry(request) {
setTimeout(function () {
openAndSend(request)
}, 1000)
}
function openAndSend() {
xhr.open('GET', 'config');
xhr.send();
}
openAndSend(xhr);
})
}
_setWsConfig(wsConfig) {
this._wsConfig = wsConfig;
Events.fire('ws-config', wsConfig);
}
_connect() {
@ -69,7 +101,7 @@ class ServerConnection {
_onPairDeviceJoin(pairKey) {
if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
setTimeout(() => this._onPairDeviceJoin(pairKey), 1000);
return;
}
this.send({ type: 'pair-device-join', pairKey: pairKey });
@ -85,7 +117,7 @@ class ServerConnection {
_onJoinPublicRoom(roomId, createIfInvalid) {
if (!this._isConnected()) {
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
setTimeout(() => this._onJoinPublicRoom(roomId), 1000);
return;
}
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
@ -93,22 +125,18 @@ class ServerConnection {
_onLeavePublicRoom() {
if (!this._isConnected()) {
setTimeout(_ => this._onLeavePublicRoom(), 1000);
setTimeout(() => this._onLeavePublicRoom(), 1000);
return;
}
this.send({ type: 'leave-public-room' });
}
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
case 'ws-config':
this._setWsConfig(msg.wsConfig);
break;
case 'peers':
this._onPeers(msg);
@ -158,6 +186,25 @@ class ServerConnection {
case 'public-room-left':
Events.fire('public-room-left');
break;
case 'request':
case 'header':
case 'partition':
case 'partition-received':
case 'progress':
case 'files-transfer-response':
case 'file-transfer-complete':
case 'message-transfer-complete':
case 'text':
case 'display-name-changed':
case 'ws-chunk':
// ws-fallback
if (this._wsConfig.wsFallback) {
Events.fire('ws-relay', JSON.stringify(msg));
}
else {
console.log("WS receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.")
}
break;
default:
console.error('WS receive: unknown message type', msg);
}
@ -175,45 +222,57 @@ class ServerConnection {
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem('peer_id', msg.message.peerId);
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
sessionStorage.setItem('peer_id', msg.peerId);
sessionStorage.setItem('peer_id_hash', msg.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
BrowserTabsConnector
.addPeerIdToLocalStorage()
.then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets()
.then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
});
Events.fire('display-name', msg);
}
_endpoint() {
// hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
// Check whether the instance specifies another signaling server otherwise use the current instance for signaling
let wsServerDomain = this._config.signalingServer
? this._config.signalingServer
: location.host + location.pathname;
let wsUrl = new URL(protocol + '://' + wsServerDomain + 'server');
wsUrl.searchParams.append('webrtc_supported', window.isRtcSupported ? 'true' : 'false');
const peerId = sessionStorage.getItem('peer_id');
const peerIdHash = sessionStorage.getItem('peer_id_hash');
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
wsUrl.searchParams.append('peer_id', peerId);
wsUrl.searchParams.append('peer_id_hash', peerIdHash);
}
return ws_url.toString();
return wsUrl.toString();
}
_disconnect() {
this.send({ type: 'disconnect' });
const peerId = sessionStorage.getItem('peer_id');
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
BrowserTabsConnector
.removePeerIdFromLocalStorage(peerId)
.then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
@ -229,7 +288,7 @@ class ServerConnection {
setTimeout(() => {
this._isReconnect = true;
Events.fire('ws-disconnected');
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
this._reconnectTimer = setTimeout(() => this._connect(), 1000);
}, 100); //delay for 100ms to prevent flickering on page reload
}
@ -281,6 +340,9 @@ class Peer {
this._send(JSON.stringify(message));
}
// Is overwritten in expanding classes
_send(message) {}
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
@ -297,16 +359,27 @@ class Peer {
return this._roomIds['secret'];
}
_regenerationOfPairSecretNeeded() {
return this._getPairSecret() && this._getPairSecret().length !== 256
}
_getRoomTypes() {
return Object.keys(this._roomIds);
}
_updateRoomIds(roomType, roomId) {
const roomTypeIsSecret = roomType === "secret";
const roomIdIsNotPairSecret = this._getPairSecret() !== roomId;
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
if (!this._isSameBrowser()
&& roomTypeIsSecret
&& this._isPaired()
&& roomIdIsNotPairSecret) {
// multiple roomSecrets with same peer -> delete old roomSecret
PersistentStorage.deleteRoomSecret(this._getPairSecret())
PersistentStorage
.deleteRoomSecret(this._getPairSecret())
.then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
});
@ -314,8 +387,13 @@ class Peer {
this._roomIds[roomType] = roomId;
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
if (!this._isSameBrowser()
&& roomTypeIsSecret
&& this._isPaired()
&& this._regenerationOfPairSecretNeeded()
&& this._isCaller) {
// increase security by initiating the increase of the roomSecret length
// from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._getPairSecret());
}
@ -336,7 +414,8 @@ class Peer {
return;
}
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
PersistentStorage
.getRoomSecretEntry(this._getPairSecret())
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry
? roomSecretEntry.entry.auto_accept
@ -367,13 +446,16 @@ class Peer {
if (width && height) {
canvas.width = width;
canvas.height = height;
} else if (width) {
}
else if (width) {
canvas.width = width;
canvas.height = Math.floor(imageHeight * width / imageWidth)
} else if (height) {
}
else if (height) {
canvas.width = Math.floor(imageWidth * height / imageHeight);
canvas.height = height;
} else {
}
else {
canvas.width = imageWidth;
canvas.height = imageHeight
}
@ -385,9 +467,11 @@ class Peer {
resolve(dataUrl);
}
image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`);
}).then(dataUrl => {
})
.then(dataUrl => {
return dataUrl;
}).catch(e => console.error(e));
})
.catch(e => console.error(e));
}
async requestFileTransfer(files) {
@ -622,7 +706,8 @@ class Peer {
this._busy = false;
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else {
}
else {
this._dequeueFile();
}
}
@ -673,9 +758,12 @@ class Peer {
class RTCPeer extends Peer {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
constructor(serverConnection, isCaller, peerId, roomType, roomId, rtcConfig) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = true;
this.rtcConfig = rtcConfig
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
@ -685,13 +773,14 @@ class RTCPeer extends Peer {
if (this._isCaller) {
this._openChannel();
} else {
}
else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn = new RTCPeerConnection(this.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
@ -708,14 +797,16 @@ class RTCPeer extends Peer {
channel.onopen = e => this._onChannelOpened(e);
channel.onerror = e => this._onError(e);
this._conn.createOffer()
this._conn
.createOffer()
.then(d => this._onDescription(d))
.catch(e => this._onError(e));
}
_onDescription(description) {
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
this._conn.setLocalDescription(description)
this._conn
.setLocalDescription(description)
.then(_ => this._sendSignal({ sdp: description }))
.catch(e => this._onError(e));
}
@ -729,16 +820,20 @@ class RTCPeer extends Peer {
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
.then( _ => {
this._conn
.setRemoteDescription(message.sdp)
.then(_ => {
if (message.sdp.type === 'offer') {
return this._conn.createAnswer()
return this._conn
.createAnswer()
.then(d => this._onDescription(d));
}
})
.catch(e => this._onError(e));
} else if (message.ice) {
this._conn.addIceCandidate(new RTCIceCandidate(message.ice))
}
else if (message.ice) {
this._conn
.addIceCandidate(new RTCIceCandidate(message.ice))
.catch(e => this._onError(e));
}
}
@ -879,6 +974,48 @@ class RTCPeer extends Peer {
}
}
class WSPeer extends Peer {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = false;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal();
}
_send(chunk) {
this.sendJSON({
type: 'ws-chunk',
chunk: arrayBufferToBase64(chunk)
});
}
sendJSON(message) {
message.to = this._peerId;
message.roomType = this._getRoomTypes()[0];
message.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(message);
}
_sendSignal(connected = false) {
this.sendJSON({type: 'signal', connected: connected});
}
onServerMessage(message) {
this._peerId = message.sender.id;
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (message.connected) return;
this._sendSignal(true);
}
getConnectionHash() {
// Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys
return "";
}
}
class PeersManager {
constructor(serverConnection) {
@ -902,10 +1039,17 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
Events.on('ws-config', e => this._onWsConfig(e.detail));
}
_onWsConfig(wsConfig) {
this._wsConfig = wsConfig;
}
_onMessage(message) {
@ -913,9 +1057,10 @@ class PeersManager {
this.peers[peerId].onServerMessage(message);
}
_refreshPeer(peer, roomType, roomId) {
if (!peer) return false;
_refreshPeer(peerId, roomType, roomId) {
if (!this._peerExists(peerId)) return false;
const peer = this.peers[peerId];
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
@ -933,26 +1078,42 @@ class PeersManager {
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomId);
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
if (this._peerExists(peerId)) {
this._refreshPeer(peerId, roomType, roomId);
return;
}
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);
}
else if (this._wsConfig.wsFallback) {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
}
else {
console.warn("Websocket fallback is not activated on this instance.\n" +
"Activate WebRTC in this browser or ask the admin of this instance to activate the websocket fallback.")
}
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
})
}
_onWsRelay(message) {
if (!this._wsConfig.wsFallback) return;
const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message);
}
_onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
}
@ -978,6 +1139,9 @@ class PeersManager {
}
_onPeerLeft(message) {
if (this._peerExists(message.peerId) && this._webRtcSupported(message.peerId)) {
console.log('WSPeer left:', message.peerId);
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
@ -985,10 +1149,12 @@ class PeersManager {
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
BrowserTabsConnector
.removeOtherPeerIdsFromLocalStorage()
.then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@ -997,6 +1163,24 @@ class PeersManager {
this._notifyPeerDisplayNameChanged(peerId);
}
_peerExists(peerId) {
return !!this.peers[peerId];
}
_webRtcSupported(peerId) {
return this.peers[peerId].rtcSupported
}
_onWsDisconnected() {
if (!this._wsConfig || !this._wsConfig.wsFallback) return;
for (const peerId in this.peers) {
if (!this._webRtcSupported(peerId)) {
Events.fire('peer-disconnected', peerId);
}
}
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];
@ -1038,16 +1222,19 @@ class PeersManager {
if (peer._getRoomTypes().length > 1) {
peer._removeRoomType(roomType);
} else {
}
else {
Events.fire('peer-disconnected', peerId);
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
PersistentStorage
.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret)
.then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
@ -1173,17 +1360,3 @@ class FileDigester {
}
}
class Events {
static fire(type, detail = {}) {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}
static on(type, callback, options = false) {
return window.addEventListener(type, callback, options);
}
static off(type, callback, options = false) {
return window.removeEventListener(type, callback, options);
}
}

View file

@ -0,0 +1,299 @@
class PersistentStorage {
constructor() {
if (!('indexedDB' in window)) {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
DBOpenRequest.onerror = e => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
console.log(e)
};
DBOpenRequest.onsuccess = _ => {
console.log('Database initialised.');
};
DBOpenRequest.onupgradeneeded = e => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
db.createObjectStore('keyval');
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
}
if (e.oldVersion <= 1) {
// migrate to v2
db.createObjectStore('share_target_files');
}
if (e.oldVersion <= 2) {
// migrate to v3
db.deleteObjectStore('share_target_files');
db.createObjectStore('share_target_files', {autoIncrement: true});
}
if (e.oldVersion <= 3) {
// migrate to v4
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
}
}
}
static logBrowserNotCapable() {
console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
}
static set(key, value) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.put(value, key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
resolve(value);
};
}
DBOpenRequest.onerror = e => {
reject(e);
}
})
}
static get(key) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readonly');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
resolve(objectStoreRequest.result);
}
}
DBOpenRequest.onerror = e => {
reject(e);
}
});
}
static delete(key) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.delete(key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Deleted key: ${key}`);
resolve();
};
}
DBOpenRequest.onerror = e => {
reject(e);
}
})
}
static addRoomSecret(roomSecret, displayName, deviceName) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.add({
'secret': roomSecret,
'display_name': displayName,
'device_name': deviceName,
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
DBOpenRequest.onerror = e => {
reject(e);
}
})
}
static async getAllRoomSecrets() {
try {
const roomSecrets = await this.getAllRoomSecretEntries();
let secrets = [];
for (let i = 0; i < roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
} catch (e) {
this.logBrowserNotCapable();
return 0;
}
}
static getAllRoomSecretEntries() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.getAll();
objectStoreRequest.onsuccess = e => {
resolve(e.target.result);
}
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static getRoomSecretEntry(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
});
}
objectStoreRequestRetrieval.onerror = (e) => {
reject(e);
}
};
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static deleteRoomSecret(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = e => {
reject(e);
}
};
}
DBOpenRequest.onerror = e => {
reject(e);
}
})
}
static clearRoomSecrets() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = _ => {
console.log('Request successful. All room_secrets cleared');
resolve();
};
}
DBOpenRequest.onerror = e => {
reject(e);
}
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
const db = e.target.result;
this.getRoomSecretEntry(roomSecret)
.then(roomSecretEntry => {
if (!roomSecretEntry) {
resolve(false);
return;
}
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key
});
}
objectStoreRequestUpdate.onerror = (e) => {
reject(e);
}
})
.catch(e => reject(e));
};
DBOpenRequest.onerror = e => reject(e);
})
}
}

View file

@ -1,78 +0,0 @@
(function(){
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
const $themeAuto = document.getElementById('theme-auto');
const $themeLight = document.getElementById('theme-light');
const $themeDark = document.getElementById('theme-dark');
let currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
setModeToDark();
} else if (currentTheme === 'light') {
setModeToLight();
}
$themeAuto.addEventListener('click', _ => {
if (currentTheme) {
setModeToAuto();
} else {
setModeToDark();
}
});
$themeLight.addEventListener('click', _ => {
if (currentTheme !== 'light') {
setModeToLight();
} else {
setModeToAuto();
}
});
$themeDark.addEventListener('click', _ => {
if (currentTheme !== 'dark') {
setModeToDark();
} else {
setModeToLight();
}
});
function setModeToDark() {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
localStorage.setItem('theme', 'dark');
currentTheme = 'dark';
$themeAuto.classList.remove("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.add("selected");
}
function setModeToLight() {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
localStorage.setItem('theme', 'light');
currentTheme = 'light';
$themeAuto.classList.remove("selected");
$themeLight.classList.add("selected");
$themeDark.classList.remove("selected");
}
function setModeToAuto() {
document.body.classList.remove('dark-theme');
document.body.classList.remove('light-theme');
if (prefersDarkTheme) {
document.body.classList.add('dark-theme');
} else if (prefersLightTheme) {
document.body.classList.add('light-theme');
}
localStorage.removeItem('theme');
currentTheme = undefined;
$themeAuto.classList.add("selected");
$themeLight.classList.remove("selected");
$themeDark.classList.remove("selected");
}
})();

286
public/scripts/ui-main.js Normal file
View file

@ -0,0 +1,286 @@
// Selector shortcuts
const $ = query => document.getElementById(query);
const $$ = query => document.querySelector(query);
// Event listener shortcuts
class Events {
static fire(type, detail = {}) {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}
static on(type, callback, options) {
return window.addEventListener(type, callback, options);
}
static off(type, callback, options) {
return window.removeEventListener(type, callback, options);
}
}
// UIs needed on start
class ThemeUI {
constructor() {
this.prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
this.$themeAutoBtn = document.getElementById('theme-auto');
this.$themeLightBtn = document.getElementById('theme-light');
this.$themeDarkBtn = document.getElementById('theme-dark');
let currentTheme = this.getCurrentTheme();
if (currentTheme === 'dark') {
this.setModeToDark();
} else if (currentTheme === 'light') {
this.setModeToLight();
}
this.$themeAutoBtn.addEventListener('click', _ => this.onClickAuto());
this.$themeLightBtn.addEventListener('click', _ => this.onClickLight());
this.$themeDarkBtn.addEventListener('click', _ => this.onClickDark());
}
getCurrentTheme() {
return localStorage.getItem('theme');
}
setCurrentTheme(theme) {
localStorage.setItem('theme', theme);
}
onClickAuto() {
if (this.getCurrentTheme()) {
this.setModeToAuto();
} else {
this.setModeToDark();
}
}
onClickLight() {
if (this.getCurrentTheme() !== 'light') {
this.setModeToLight();
} else {
this.setModeToAuto();
}
}
onClickDark() {
if (this.getCurrentTheme() !== 'dark') {
this.setModeToDark();
} else {
this.setModeToLight();
}
}
setModeToDark() {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
this.setCurrentTheme('dark');
this.$themeAutoBtn.classList.remove("selected");
this.$themeLightBtn.classList.remove("selected");
this.$themeDarkBtn.classList.add("selected");
}
setModeToLight() {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
this.setCurrentTheme('light');
this.$themeAutoBtn.classList.remove("selected");
this.$themeLightBtn.classList.add("selected");
this.$themeDarkBtn.classList.remove("selected");
}
setModeToAuto() {
document.body.classList.remove('dark-theme');
document.body.classList.remove('light-theme');
if (this.prefersDarkTheme) {
document.body.classList.add('dark-theme');
}
else if (this.prefersLightTheme) {
document.body.classList.add('light-theme');
}
localStorage.removeItem('theme');
this.$themeAutoBtn.classList.add("selected");
this.$themeLightBtn.classList.remove("selected");
this.$themeDarkBtn.classList.remove("selected");
}
}
class FooterUI {
constructor() {
this.$displayName = $('display-name');
this.$discoveryWrapper = $$('footer .discovery-wrapper');
// Show "Loading…"
this.$displayName.setAttribute('placeholder', this.$displayName.dataset.placeholder);
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
// Load saved display name on page load
Events.on('ws-connected', _ => this._loadSavedDisplayName());
Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges());
}
_evaluateFooterBadges() {
if (this.$discoveryWrapper.querySelectorAll('div:last-of-type > span[hidden]').length < 2) {
this.$discoveryWrapper.classList.remove('row');
this.$discoveryWrapper.classList.add('column');
}
else {
this.$discoveryWrapper.classList.remove('column');
this.$discoveryWrapper.classList.add('row');
}
Events.fire('redraw-canvas');
Events.fire('fade-in-ui');
}
_loadSavedDisplayName() {
this._getSavedDisplayName()
.then(displayName => {
console.log("Retrieved edited display name:", displayName)
if (displayName) {
Events.fire('self-display-name-changed', displayName);
}
});
}
_onDisplayName(displayName){
// set display name
this.$displayName.setAttribute('placeholder', displayName);
}
_insertDisplayName(displayName) {
this.$displayName.textContent = displayName;
}
_onKeyDownDisplayName(e) {
if (e.key === "Enter" || e.key === "Escape") {
e.preventDefault();
e.target.blur();
}
}
_onKeyUpDisplayName(e) {
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
}
async _saveDisplayName(newDisplayName) {
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
const savedDisplayName = await this._getSavedDisplayName();
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => {
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
})
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
})
.finally(() => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
}
else {
PersistentStorage.delete('editedDisplayName')
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
})
.finally(() => {
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
_getSavedDisplayName() {
return new Promise((resolve) => {
PersistentStorage.get('editedDisplayName')
.then(displayName => {
if (!displayName) displayName = "";
resolve(displayName);
})
.catch(_ => {
let displayName = localStorage.getItem('editedDisplayName');
if (!displayName) displayName = "";
resolve(displayName);
})
});
}
}
class BackgroundCanvas {
constructor() {
this.c = $$('canvas');
this.cCtx = this.c.getContext('2d');
this.$footer = $$('footer');
// fade-in on load
Events.on('fade-in-ui', _ => this._fadeIn());
// redraw canvas
Events.on('resize', _ => this.init());
Events.on('redraw-canvas', _ => this.init());
Events.on('translation-loaded', _ => this.init());
}
_fadeIn() {
this.c.classList.remove('opacity-0');
}
init() {
let oldW = this.w;
let oldH = this.h;
let oldOffset = this.offset
this.w = document.documentElement.clientWidth;
this.h = document.documentElement.clientHeight;
this.offset = this.$footer.offsetHeight - 27;
if (this.h >= 800) this.offset += 10;
if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed
this.c.width = this.w;
this.c.height = this.h;
this.x0 = this.w / 2;
this.y0 = this.h - this.offset;
this.dw = Math.round(Math.max(this.w, this.h, 1000) / 13);
this.drawCircles(this.cCtx);
}
drawCircle(ctx, radius) {
ctx.beginPath();
ctx.lineWidth = 2;
let opacity = Math.max(0, 0.3 * (1 - 1.2 * radius / Math.max(this.w, this.h)));
ctx.strokeStyle = `rgba(165, 165, 165, ${opacity})`;
ctx.arc(this.x0, this.y0, radius, 0, 2 * Math.PI);
ctx.stroke();
}
drawCircles(ctx) {
ctx.clearRect(0, 0, this.w, this.h);
for (let i = 0; i < 13; i++) {
this.drawCircle(ctx, this.dw * i + 33 + 66);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,31 @@ if (!navigator.clipboard) {
}
}
// Polyfills
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document
? 'hidden'
: 'webkitHidden' in document
? 'webkitHidden'
: 'mozHidden' in document
? 'mozHidden'
: null;
window.visibilityChangeEvent = 'visibilitychange' in document
? 'visibilitychange'
: 'webkitvisibilitychange' in document
? 'webkitvisibilitychange'
: 'mozvisibilitychange' in document
? 'mozvisibilitychange'
: null;
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
window.isMobile = window.iOS || window.android;
// Helper functions
const zipper = (() => {
let zipWriter;
@ -52,7 +77,8 @@ const zipper = (() => {
const blobURL = URL.createObjectURL(await zipWriter.close());
zipWriter = null;
return blobURL;
} else {
}
else {
throw new Error("Zip file closed");
}
},
@ -61,7 +87,8 @@ const zipper = (() => {
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
zipWriter = null;
return file;
} else {
}
else {
throw new Error("Zip file closed");
}
},
@ -411,3 +438,23 @@ function changeFavicon(src) {
document.querySelector('[rel="icon"]').href = src;
document.querySelector('[rel="shortcut icon"]').href = src;
}
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa( binary );
}
function base64ToArrayBuffer(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}

View file

@ -1,20 +1,24 @@
const cacheVersion = 'v1.9.4';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
const urlsToCache = [
const relativePathsToCache = [
'./',
'index.html',
'manifest.json',
'styles.css',
'styles/styles-main.css',
'styles/deferred-styles.css',
'scripts/localization.js',
'scripts/main.js',
'scripts/network.js',
'scripts/NoSleep.min.js',
'scripts/QRCode.min.js',
'scripts/theme.js',
'scripts/no-sleep.min.js',
'scripts/persistent-storage.js',
'scripts/qr-code.min.js',
'scripts/ui.js',
'scripts/ui-main.js',
'scripts/util.js',
'scripts/zip.min.js',
'sounds/blop.mp3',
'sounds/blop.ogg',
'images/favicon-96x96.png',
'images/favicon-96x96-notification.png',
'images/android-chrome-192x192.png',
@ -32,32 +36,49 @@ const urlsToCache = [
'lang/ja.json',
'lang/nb.json',
'lang/nl.json',
'lang/tr.json',
'lang/ro.json',
'lang/ru.json',
'lang/zh-CN.json'
];
const relativePathsNotToCache = [
'config'
]
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(cacheTitle)
.then(function(cache) {
return cache.addAll(urlsToCache).then(_ => {
console.log('All files cached.');
});
return cache
.addAll(relativePathsToCache)
.then(_ => {
console.log('All files cached.');
});
})
);
});
// fetch the resource from the network
const fromNetwork = (request, timeout) =>
new Promise((fulfill, reject) => {
new Promise((resolve, reject) => {
const timeoutId = setTimeout(reject, timeout);
fetch(request).then(response => {
clearTimeout(timeoutId);
fulfill(response);
update(request).then(() => console.log("Cache successfully updated for", request.url));
}, reject);
fetch(request)
.then(response => {
clearTimeout(timeoutId);
resolve(response);
if (doNotCacheRequest(request)) return;
update(request)
.then(() => console.log("Cache successfully updated for", request.url))
.catch(reason => console.log("Cache could not be updated for", request.url, "Reason:", reason));
})
.catch(error => {
// Handle any errors that occurred during the fetch
console.error(`Could not fetch ${request.url}. Are you online?`);
reject(error);
});
});
// fetch the resource from the browser cache
@ -68,17 +89,32 @@ const fromCache = request =>
cache.match(request)
);
const rootUrl = location.href.substring(0, location.href.length - "service-worker.js".length);
const rootUrlLength = rootUrl.length;
const doNotCacheRequest = request => {
const requestRelativePath = request.url.substring(rootUrlLength);
return relativePathsNotToCache.indexOf(requestRelativePath) !== -1
};
// cache the current page to make it available for offline
const update = request =>
const update = request => new Promise((resolve, reject) => {
if (doNotCacheRequest(request)) {
reject("Url is specifically prevented from being cached in the serviceworker.");
return;
}
caches
.open(cacheTitle)
.then(cache =>
fetch(request)
.then(async response => {
await cache.put(request, response);
fetch(request, {cache: "no-store"})
.then(response => {
cache
.put(request, response)
.then(() => resolve());
})
.catch(() => console.log(`Cache could not be updated. ${request.url}`))
.catch(reason => reject(reason))
);
});
// general strategy when making a request (eg if online try to fetch it
// from cache, if something fails fetch from network. Update cache everytime files are fetched.
@ -90,16 +126,19 @@ self.addEventListener('fetch', function(event) {
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
}
else {
// Regular requests not related to Web Share Target.
if (forceFetch) {
event.respondWith(fromNetwork(event.request, 10000));
} else {
}
else {
event.respondWith(
fromCache(event.request).then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
fromCache(event.request)
.then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
);
}
}
@ -109,15 +148,16 @@ self.addEventListener('fetch', function(event) {
// on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt => {
return evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) {
return caches.delete(cacheName);
}
})
);
})
)
}
);
@ -157,7 +197,8 @@ const evaluateRequestData = function (request) {
DBOpenRequest.onerror = _ => {
resolve(pairDropUrl);
}
} else {
}
else {
let urlArgument = '?share-target=text';
if (title) urlArgument += `&title=${title}`;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,735 @@
/* All styles in this sheet are not needed on page load and deferred */
/* Paste mode */
#cancel-paste-mode {
z-index: 21;
margin: 0;
padding: 0;
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100vw;
height: 56px;
background-color: var(--primary-color);
color: rgb(238, 238, 238);
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
outline: none;
padding: 16px 24px;
border-radius: 12px;
font-size: inherit;
font-family: inherit;
display: block;
overflow: auto;
resize: none;
line-height: 16px;
max-height: 300px;
word-break: break-word;
word-wrap: anywhere;
}
/* Peers */
x-peers:has(> x-peer) {
--peers-per-row: 10;
}
@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),
screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(7)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 10;
}
}
@media screen and (min-height: 649px) and (max-width: 425px),
screen and (min-height: 631px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(28)) {
--peers-per-row: 10;
}
}
/* Peer */
x-peer {
padding: 8px;
align-content: start;
flex-wrap: wrap;
}
x-peer input[type="file"] {
visibility: hidden;
position: absolute;
}
x-peer label {
width: var(--peer-width);
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
x-peer x-icon {
--icon-size: 40px;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex;
flex-direction: column;
}
x-peer .icon-wrapper {
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--accent-color);
background-image: linear-gradient(45deg, var(--accent-color) 40%, color-mix(in srgb, var(--accent-color) 70%, white) 100%);
color: white;
display: flex;
}
x-peer.type-secret .icon-wrapper {
--accent-color: var(--paired-device-color);
}
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
--accent-color: var(--public-room-color);
}
.highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
}
.highlight {
width: 15px;
height: 6px;
border-radius: 4px;
margin-left: 1px;
margin-right: 1px;
--highlight-color: var(--badge-color);
background-color: var(--highlight-color);
background-image: linear-gradient(180deg, var(--highlight-color) 0%, color-mix(in srgb, var(--highlight-color) 90%, black));
}
.highlight-room-ip {
--highlight-color: var(--primary-color);
}
.highlight-room-secret {
--highlight-color: var(--paired-device-color);
}
.highlight-room-public-id {
--highlight-color: var(--public-room-color);
}
x-peer:not(.type-ip) .highlight-room-ip {
display: none;
}
x-peer:not(.type-secret) .highlight-room-secret {
display: none;
}
x-peer:not(.type-public-id) .highlight-room-public-id {
display: none;
}
x-peer:not([status]):hover x-icon,
x-peer:not([status]):focus x-icon {
transform: scale(1.05);
}
x-peer[status] x-icon {
box-shadow: none;
opacity: 0.8;
transform: scale(1);
}
x-peer.ws-peer {
margin-top: -1.5px;
}
x-peer.ws-peer .progress {
margin-top: 3px;
}
x-peer.ws-peer .icon-wrapper{
border: solid 3px var(--ws-peer-color);
}
x-peer.ws-peer .highlight-wrapper {
margin-top: 3px;
}
#websocket-fallback {
opacity: 0.5;
}
#websocket-fallback > span:nth-of-type(2) {
border-bottom: solid 2px var(--ws-peer-color);
}
.device-descriptor {
width: 100%;
text-align: center;
}
.device-descriptor > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status,
.device-name {
opacity: 0.7;
white-space: nowrap;
}
x-peer:not([status]) .status,
x-peer[status] .device-name {
display: none;
}
x-peer[status] {
pointer-events: none;
}
x-peer x-icon {
animation: pop 600ms ease-out 1;
}
@keyframes pop {
0% {
transform: scale(0.7);
}
40% {
transform: scale(1.2);
}
}
x-peer[drop] x-icon {
transform: scale(1.1);
}
/* Dialog */
x-dialog x-background {
background: rgba(0, 0, 0, 0.8);
z-index: 30;
transition: opacity 300ms;
will-change: opacity;
overflow: overlay;
}
x-dialog x-paper {
display: flex;
flex-direction: column;
width: calc(100vw - 10px);
z-index: 3;
border-radius: 30px;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pair-device-dialog x-paper,
#edit-paired-devices-dialog x-paper,
#public-room-dialog x-paper,
#language-select-dialog x-paper {
position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
}
x-paper > .row:first-of-type {
background-color: var(--accent-color);
padding: 10px;
margin-bottom: 5px;
}
x-paper > .row:first-of-type h2 {
color: white;
}
#pair-device-dialog,
#edit-paired-devices-dialog {
--accent-color: var(--paired-device-color);
}
#public-room-dialog {
--accent-color: var(--public-room-color);
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
#public-room-dialog ::-moz-selection,
#public-room-dialog ::selection {
color: black;
background: var(--public-room-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
x-dialog:not([show]) x-paper {
transform: scale(0.1);
}
x-dialog a {
color: var(--primary-color);
}
/* Pair Devices Dialog & Public Room Dialog */
.input-key-container {
width: 100%;
display: flex;
justify-content: center;
}
.input-key-container > input {
width: 45px;
height: 45px;
font-size: 30px;
padding: 0;
text-align: center;
text-transform: uppercase;
display: -webkit-box !important;
display: -webkit-flex !important;
display: -moz-flex !important;
display: -ms-flexbox !important;
display: flex !important;
-webkit-justify-content: center;
-ms-justify-content: center;
justify-content: center;
}
.input-key-container > input {
margin: 0 3px;
}
.input-key-container.six-chars > input:nth-of-type(4) {
margin-left: 5%;
}
.key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
display: inline-block;
font-size: 45px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 20px);
text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 28px)));
margin: 10px 0;
}
.key-qr-code {
width: fit-content;
align-self: center;
margin-top: 15px;
margin-bottom: 10px;
}
.key-instructions {
flex-direction: column;
margin: 0;
}
x-dialog h2 {
margin-top: 5px;
margin-bottom: 0;
}
x-dialog hr {
height: 1px;
border: none;
width: 100%;
background-color: var(--border-color);
}
.hr-note {
margin-top: 23px;
margin-bottom: 31px;
}
.hr-note hr {
margin-bottom: -1px;
}
.hr-note > div {
height: 0;
transform: translateY(-10px);
}
.hr-note > div > span {
padding: 3px 10px;
border-radius: 20px;
color: rgb(var(--text-color));
background-color: var(--dialog-bg-color);
border: var(--border-color) solid 3px;
text-transform: uppercase;
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: attr(data-empty);
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
margin-top: -5px;
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
/* button row*/
x-paper > .button-row {
height: 50px;
margin: 5px 10px 10px;
}
x-paper > .button-row > .btn {
height: 100%;
width: 100%;
}
html:not([dir="rtl"]) x-paper > .button-row > .btn:not(:first-child) {
margin-right: 5px;
}
html:not([dir="rtl"]) x-paper > .button-row > .btn:not(:last-child) {
margin-left: 5px;
}
html[dir="rtl"] x-paper > .button-row > .btn:not(:first-child) {
margin-right: 5px;
}
html[dir="rtl"] x-paper > .button-row > .btn:not(:last-child) {
margin-left: 5px;
}
.language-buttons > button > span {
margin: 0 0.3em;
}
.language-buttons > button {
min-height: 36px;
}
.file-description {
max-width: 100%;
}
.file-description span {
display: inline;
word-break: normal;
}
.file-name {
font-style: italic;
max-width: 100%;
margin-top: 5px;
}
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 1px;
}
/* Send Text Dialog */
x-dialog .dialog-subheader {
padding-top: 16px;
padding-bottom: 16px;
}
.display-name-wrapper {
padding-bottom: 0;
}
#send-text-dialog,
#receive-text-dialog {
font-size: 16px; /* prevents auto-zoom on edit */
--shadow-color-rgb: var(--shadow-color-secondary-rgb);
--shadow-color-cover-rgb: var(--shadow-color-secondary-cover-rgb);
}
#edit-paired-devices-dialog {
--shadow-color-rgb: var(--shadow-color-dialog-rgb);
--shadow-color-cover-rgb: var(--shadow-color-dialog-cover-rgb);
}
#text-input:before {
opacity: 0.5;
}
/* Receive Text Dialog */
#receive-text-dialog #text {
word-break: break-all;
max-height: 400px;
padding: 10px;
overflow-x: hidden;
overflow-y: scroll;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
#receive-text-dialog #text a:hover {
text-decoration: underline;
}
#receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
}
#base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
}
#base64-paste-dialog .textarea {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 14px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
white-space: pre-wrap;
}
/* Peer loading Indicator */
.progress {
width: 80px;
height: 80px;
position: absolute;
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
}
.circle {
width: 72px;
height: 72px;
border: 4px solid var(--primary-color);
border-radius: 40px;
position: absolute;
clip: rect(0px, 40px, 80px, 0px);
will-change: transform;
transform: var(--progress);
}
.over50 {
clip: rect(auto, auto, auto, auto);
}
.over50 .circle.right {
transform: rotate(180deg);
}
/*
Color Themes
*/
/* Colored Elements */
x-dialog x-paper {
background-color: var(--dialog-bg-color);
}
.textarea {
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
.textarea * {
margin: 0 !important;
padding: 0 !important;
color: unset !important;
background: unset !important;
border: unset !important;
opacity: unset !important;
font-family: inherit !important;
font-size: inherit !important;
font-style: unset !important;
font-weight: unset !important;
}
/* Image/Video/Audio Preview */
.file-preview {
margin-bottom: 15px;
}
.file-preview:empty {
display: none;
}
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;
display: block;
}

View file

@ -0,0 +1,970 @@
/* All styles in this sheet are needed on page load */
/* Layout */
html,
body {
margin: 0;
display: flex;
flex-direction: column;
width: 100vw;
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
transition: color 300ms;
}
body {
height: 100%;
}
html {
height: 100%;
}
.fw {
width: 100%;
}
.p1 {
padding: 10px;
}
.row-reverse {
display: flex;
flex-direction: row-reverse;
}
.space-between {
justify-content: space-between;
}
.row {
display: flex;
flex-direction: row;
}
.column {
display: flex;
flex-direction: column;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.grow {
flex-grow: 1;
}
.full {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.pointer {
cursor: pointer;
}
header {
position: absolute;
align-items: baseline;
padding: 8px 12px;
box-sizing: border-box;
width: 100vw;
z-index: 20;
top: 0;
right: 0;
}
header > * {
margin-left: 4px;
margin-right: 4px;
}
header > div {
display: flex;
flex-direction: column;
align-self: flex-start;
touch-action: manipulation;
}
header > div .icon-button {
height: 40px;
transition: all 300ms;
}
header > div > div {
display: flex;
flex-direction: column;
}
header > div:not(:hover) .icon-button:not(.selected) {
height: 0;
opacity: 0;
}
#theme-wrapper:hover::before {
border-radius: 20px;
background: currentColor;
opacity: 0.1;
transition: opacity 300ms;
content: '';
position: absolute;
width: 40px;
top: 0;
bottom: 0;
margin-top: 8px;
margin-bottom: 8px;
}
header > div:hover .icon-button.selected::before {
opacity: 0.1;
}
@media (pointer: coarse) {
header > div:hover .icon-button.selected:hover::before {
opacity: 0.2;
}
header > div .icon-button:not(.selected) {
height: 0;
opacity: 0;
pointer-events: none;
}
header > div > div {
flex-direction: column-reverse;
}
}
[hidden] {
display: none !important;
}
/* Typography */
@font-face {
font-family: "Open Sans";
src: url('../fonts/OpenSans/static/OpenSans-Medium.ttf') format('truetype');
}
body {
font-family: "Open Sans", -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-variant-ligatures: common-ligatures;
font-kerning: normal;
}
h1 {
font-size: 34px;
font-weight: 400;
letter-spacing: -.01em;
line-height: 40px;
margin: 0 0 4px;
}
h2 {
font-size: 24px;
font-weight: 400;
letter-spacing: -.012em;
line-height: 32px;
color: var(--primary-color);}
h3 {
font-size: 20px;
font-weight: 500;
margin: 16px 0;
color: var(--primary-color);
}
.font-subheading {
font-size: 14px;
font-weight: 400;
line-height: 18px;
word-break: normal;
}
.text-center {
text-align: center;
}
.font-body1,
body {
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.font-body2 {
font-size: 12px;
line-height: 18px;
}
a,
.icon-button {
text-decoration: none;
color: currentColor;
cursor: pointer;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
z-index: 2;
}
/* Icons */
.icon {
width: var(--icon-size);
height: var(--icon-size);
fill: currentColor;
}
/* Shadows */
[shadow="1"] {
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
0 1px 8px 0 rgba(0, 0, 0, 0.12),
0 3px 3px -2px rgba(0, 0, 0, 0.4);
}
[shadow="2"] {
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12),
0 2px 4px -1px rgba(0, 0, 0, 0.4);
}
.overflowing {
background:
/* Shadow covers */
linear-gradient(rgb(var(--shadow-color-cover-rgb)) 30%, rgba(var(--shadow-color-cover-rgb), 0)),
linear-gradient(rgba(var(--shadow-color-cover-rgb), 0), rgb(var(--shadow-color-cover-rgb)) 70%) 0 100%,
/* Shadows */
radial-gradient(farthest-side at 50% 0, rgba(var(--shadow-color-rgb), .2), rgba(var(--shadow-color-rgb), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--shadow-color-rgb), .2), rgba(var(--shadow-color-rgb), 0))
0 100%;
background-repeat: no-repeat;
background-size: 100% 60px, 100% 60px, 100% 24px, 100% 24px;
background-attachment: local, local, scroll, scroll;
}
/* Animations */
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#center {
position: relative;
display: flex;
margin-top: 56px;
flex-direction: column-reverse;
flex-grow: 1;
justify-content: space-around;
align-items: center;
overflow-x: hidden;
overflow-y: scroll;
overscroll-behavior-x: none;
}
/* Peers */
#x-peers-filler {
display: flex;
flex-grow: 1;
}
x-peers {
position: relative;
display: flex;
flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2;
transition: background-color 0.5s ease;
overflow-y: scroll;
overflow-x: hidden;
overscroll-behavior-x: none;
scrollbar-width: none;
--peers-per-row: 6; /* default if browser does not support :has selector */
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
width: var(--x-peers-width);
margin-right: 20px;
margin-left: 20px;
}
/* Empty Peers List */
x-no-peers {
display: flex;
flex-direction: column;
padding: 8px;
height: 137px;
text-align: center;
}
x-no-peers h2,
x-no-peers a {
color: var(--primary-color);
margin-bottom: 5px;
}
x-peers:not(:empty)+x-no-peers {
display: none;
}
x-no-peers::before {
color: var(--primary-color);
font-size: 24px;
font-weight: 400;
letter-spacing: -.012em;
line-height: 32px;
}
x-no-peers[drop-bg]::before {
content: attr(data-drop-bg);
}
x-no-peers[drop-bg] * {
display: none;
}
/* Footer */
footer {
position: relative;
z-index: 2;
align-items: center;
text-align: center;
cursor: default;
margin: auto 5px 5px;
}
footer .logo {
--icon-size: 80px;
margin-bottom: 8px;
color: var(--primary-color);
margin-top: -10px;
}
.discovery-wrapper {
font-size: 14px;
margin: 15px auto auto;
border: 2px solid var(--border-color);
padding: 2px;
background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease;
min-height: 24px;
}
.discovery-wrapper.column {
border-radius: 16px;
}
.discovery-wrapper.row {
border-radius: 12px;
}
/*You can be discovered wrapper*/
.discovery-wrapper > div:first-of-type {
padding-left: 4px;
padding-right: 4px;
}
.discovery-wrapper .badge {
word-break: keep-all;
margin: 2px;
}
.badge {
border-radius: 0.4rem;
padding-right: 0.3rem;
padding-left: 0.3em;
background-color: var(--badge-color);
color: white;
white-space: nowrap;
}
.badge-gradient {
background-image: linear-gradient(180deg, color-mix(in srgb, var(--badge-color) 80%, white) 0%, var(--badge-color) 50%);
}
.badge-room-ip {
--badge-color: var(--primary-color);
}
.badge-room-secret {
--badge-color: var(--paired-device-color);
}
.badge-room-public-id {
--badge-color: var(--public-room-color);
}
.known-as-wrapper {
font-size: 16px; /* prevents auto-zoom on edit */
}
#display-name {
position: relative;
display: inline-block;
text-align: left;
border: none;
outline: none;
max-width: 15em;
text-overflow: ellipsis;
cursor: text;
margin-bottom: -6px;
padding-bottom: 0.1rem;
border-radius: 1.3rem/30%;
border-right: solid 1rem transparent;
border-left: solid 1rem transparent;
background-clip: padding-box;
overflow: hidden;
z-index: 1;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-bottom: -2px;
position: relative;
}
html:not([dir="rtl"]) #display-name,
html:not([dir="rtl"]) #edit-pen {
margin-left: -1rem;
}
html[dir="rtl"] #display-name,
html[dir="rtl"] #edit-pen {
margin-right: -1rem;
}
html[dir="rtl"] #edit-pen {
transform: rotateY(180deg);
}
/* Dialogs needed on page load */
x-dialog:not([show]) x-background {
opacity: 0;
}
/* Button */
.btn {
font-family: "Open Sans", -apple-system, BlinkMacSystemFont, sans-serif;
padding: 2px 16px 0;
box-sizing: border-box;
font-size: 14px;
line-height: 24px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
user-select: none;
background: inherit;
color: var(--accent-color);
overflow: hidden;
}
.btn[disabled] {
color: var(--btn-disabled-color);
cursor: not-allowed;
}
.btn,
.icon-button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
touch-action: manipulation;
border: none;
outline: none;
}
.btn:before,
.icon-button:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
background-color: var(--accent-color);
transition: opacity 300ms;
}
.btn:not([disabled]):hover:before,
.icon-button:hover:before {
opacity: 0.1;
}
.btn[selected],
.icon-button[selected] {
opacity: 0.1;
}
.btn:focus:before,
.icon-button:focus:before {
opacity: 0.2;
}
.btn-rounded {
border-radius: 12px;
}
.btn-grey {
background-color: var(--bg-color-secondary);
}
button::-moz-focus-inner {
border: 0;
}
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
}
.icon-button:before {
border-radius: 50%;
}
/* Info Animation */
#about {
color: white;
z-index: 32;
overflow: hidden;
pointer-events: none;
text-align: center;
}
#about header {
z-index: 1;
}
#about:not(:target) header {
transition-delay: 400ms;
}
#about:target header {
transition-delay: 100ms;
}
#about > * {
transition: opacity 300ms ease 300ms;
will-change: opacity;
pointer-events: all;
}
#about:not(:target) > header,
#about:not(:target) > section {
opacity: 0;
pointer-events: none;
transition-delay: 0s;
}
#about .logo {
--icon-size: 96px;
}
#about .title-wrapper {
display: flex;
align-items: baseline;
}
#about .title-wrapper > div {
margin-left: 0.4em;
}
#about x-background {
position: absolute;
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
--size-half: calc(var(--size)/2);
top: calc(28px - var(--size-half));
width: var(--size);
height: var(--size);
z-index: -1;
background: var(--primary-color);
background-image: radial-gradient(circle at calc(50% - 36px), var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 40%, black) 80%);
--crop-size: 0px;
clip-path: circle(var(--crop-size));
}
html:not([dir="rtl"]) #about x-background {
right: calc(36px - var(--size-half));
}
html[dir="rtl"] #about x-background {
left: calc(36px - var(--size-half));
}
/* Hack such that initial scale(0) isn't animated */
#about x-background {
will-change: clip-path;
transition: clip-path 800ms cubic-bezier(0.77, 0, 0.175, 1);
}
#about:target x-background {
--crop-size: var(--size);
}
#about .row a {
margin: 8px 8px -16px;
}
#about section {
flex-grow: 1;
}
canvas.circles {
width: 100vw;
position: absolute;
z-index: -10;
top: 0;
left: 0;
}
/* Generic placeholder */
[placeholder]:empty:before {
content: attr(placeholder);
}
/* Toast */
.toast-container {
padding: 0 8px 24px;
overflow: hidden;
pointer-events: none;
}
x-toast {
position: absolute;
min-height: 48px;
top: 50px;
width: 100%;
max-width: 344px;
background-color: rgb(var(--text-color));
color: var(--dialog-bg-color);
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
z-index: 40;
transition: opacity 200ms, transform 300ms ease-out;
cursor: default;
line-height: 24px;
border-radius: 12px;
pointer-events: all;
}
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(-100px);
}
/* Instructions */
x-instructions {
position: relative;
opacity: 0.5;
text-align: center;
margin-left: 10px;
margin-right: 10px;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
}
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(mobile);
}
x-instructions[drop-peer]:before {
content: attr(data-drop-peer);
}
x-instructions[drop-bg]:not([drop-peer]):before {
content: attr(data-drop-bg);
}
x-instructions p {
display: none;
}
x-peers:empty~x-instructions {
opacity: 0 !important;
}
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px;
}
}
/* Prevent Cumulative Layout Shift */
.fade-in {
animation: fade-in 600ms;
animation-fill-mode: backwards;
}
.no-animation-on-load {
animation-iteration-count: 0;
}
.opacity-0 {
opacity: 0;
}
/* Responsive Styles */
@media screen and (min-height: 800px) {
footer {
margin-bottom: 16px;
}
}
@media (hover: hover) and (pointer: fine) {
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(desktop);
}
}
/* Constants */
:root {
--icon-size: 24px;
--peer-width: 120px;
color-scheme: light dark;
}
/*
Color Themes
*/
/* Default colors */
body {
/* Constant colors */
--primary-color: #4285f4;
--paired-device-color: #00a69c;
--public-room-color: #db8500;
--accent-color: var(--primary-color);
--ws-peer-color: #ff6b6b;
--btn-disabled-color: #5B5B66;
/* shadows */
--shadow-color-rgb: var(--text-color);
--shadow-color-cover-rgb: var(--bg-color);
}
/* Light theme colors */
body {
--text-color: 51,51,51;
--dialog-bg-color: #fff;
--bg-color: 255,255,255;
--bg-color-secondary: #f2f2f2;
--border-color: rgb(169, 169, 169);
--badge-color: #a5a5a5;
--shadow-color-secondary-rgb: 0,0,0;
--shadow-color-secondary-cover-rgb: 242,242,242;
--shadow-color-dialog-rgb: 0,0,0;
--shadow-color-dialog-cover-rgb: 242,242,242;
}
/* Dark theme colors */
body.dark-theme {
--text-color: 238,238,238;
--dialog-bg-color: #121212;
--bg-color: 0,0,0;
--bg-color-secondary: #262628;
--border-color: rgb(91, 91, 91);
--badge-color: #717171;
--shadow-color-secondary-rgb: 255,255,255;
--shadow-color-secondary-cover-rgb: 38,38,38;
--shadow-color-dialog-rgb: 255,255,255;
--shadow-color-dialog-cover-rgb: 38,38,38;
}
/* Styles for users who prefer dark mode at the OS level */
@media (prefers-color-scheme: dark) {
/* defaults to dark theme */
body {
--text-color: 238,238,238;
--dialog-bg-color: #121212;
--bg-color-secondary: #262628;
--bg-color: 0,0,0;
--border-color: rgb(91, 91, 91);
--badge-color: #717171;
--shadow-color-secondary-rgb: 255,255,255;
--shadow-color-secondary-cover-rgb: 38,38,38;
--shadow-color-dialog-rgb: 255,255,255;
--shadow-color-dialog-cover-rgb: 38,38,38;
}
/* Override dark mode with light mode styles if the user decides to swap */
body.light-theme {
--text-color: 51,51,51;
--dialog-bg-color: #fff;
--bg-color: 255,255,255;
--bg-color-secondary: #f2f2f2;
--border-color: rgb(169, 169, 169);
--badge-color: #a5a5a5;
--shadow-color-secondary-rgb: 0,0,0;
--shadow-color-secondary-cover-rgb: 242,242,242;
--shadow-color-dialog-rgb: 0,0,0;
--shadow-color-dialog-cover-rgb: 242,242,242;
}
}
/* Colored Elements */
body {
color: rgb(var(--text-color));
background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease;
}
x-dialog x-paper {
background-color: var(--dialog-bg-color);
}
.textarea {
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
.textarea * {
margin: 0 !important;
padding: 0 !important;
color: unset !important;
background: unset !important;
border: unset !important;
opacity: unset !important;
font-family: inherit !important;
font-size: inherit !important;
font-style: unset !important;
font-weight: unset !important;
}
/* Gradient for wifi-tether icon */
#primaryGradient .start-color {
stop-color: var(--primary-color);
}
@supports (stop-color: color-mix(in srgb, blue 50%, black)) {
#primaryGradient .start-color {
stop-color: color-mix(in srgb, var(--primary-color) 80%, white);
}
}
#primaryGradient .stop-color {
stop-color: var(--primary-color);
}
/*
Edge specific styles
*/
@supports (-ms-ime-align: auto) {
html,
body {
overflow: hidden;
}
}
/*
Browser specific styles
*/
body {
/* mobile viewport bug fix */
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
html {
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
/* webkit scrollbar style*/
::-webkit-scrollbar{
width: 4px;
height: 4px;
}
::-webkit-scrollbar-thumb{
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}
/* make elements with attribute contenteditable editable on older iOS devices.
See note here: https://developer.mozilla.org/en-US/docs/Web/CSS/user-select */
[contenteditable] {
-webkit-user-select: text;
user-select: text;
}