Compare commits

..

20 Commits

Author SHA1 Message Date
schlagmichdoch
8ecec5c1bf increase version to v1.1.2 2023-02-24 18:19:49 +01:00
schlagmichdoch
78cf0139b8 Merge pull request #47 from xstar97/patch-1
Fix(ghcr) update workflow variable to a static lowercase name
2023-02-24 18:12:01 +01:00
schlagmichdoch
591c76c15a fix dialog heights 2023-02-24 18:10:34 +01:00
Xstar97TheNoob
2a3d1d4105 Fix variable to a static lowercase name 2023-02-24 10:40:30 -05:00
schlagmichdoch
5bff933b6e Merge pull request #45 from xstar97/docs-fix
docs(ghcr) add deployment notes for ghcr
2023-02-24 16:23:19 +01:00
schlagmichdoch
0ba1bd7113 tidy up Docker deployment notes 2023-02-24 16:20:19 +01:00
schlagmichdoch
0eb13d9d1b increase QR-Code size as requested in #43 and fix overflow issues on iOS 2023-02-24 16:08:36 +01:00
schlagmichdoch
ad109d1724 Merge pull request #37 from xstar97/ghcr-build-wk
feat(ghcr.io)  build container on tagged commits to ghcr.io
2023-02-23 21:00:35 +01:00
Xstar97TheNoob
f9e214a1e5 docs(ghcr) add deployment notes for ghcr 2023-02-23 12:53:04 -05:00
schlagmichdoch
36da8e3490 increase version to v1.1.0 2023-02-22 03:03:53 +01:00
schlagmichdoch
40c0735c90 touched UX to make receive dialogs more intuitive. closes #40 2023-02-22 03:01:06 +01:00
schlagmichdoch
12a2fc1b0a Merge pull request #39 from schlagmichdoch/enable_sending_from_cli
Closes #34
2023-02-22 02:37:18 +01:00
schlagmichdoch
f8d49754d2 added pairdrop-cli to documentation 2023-02-22 02:23:18 +01:00
schlagmichdoch
3cb4e6d476 pairdrop-cli now working on windows via bash pairdrop or git bash 2023-02-22 02:22:33 +01:00
schlagmichdoch
8f0e465b8e pairdrop-cli: change domain via flag, move bash file to separate folder and add console logs to ui.js 2023-02-21 23:44:41 +01:00
schlagmichdoch
ce13348aeb Merge pull request #36 from schlagmichdoch/dependabot/npm_and_yarn/ws-8.12.1
Bump ws from 8.12.0 to 8.12.1
2023-02-20 23:47:40 +01:00
schlagmichdoch
0f9bbf9bbb enable sending from cli by adding bash script 2023-02-20 17:42:02 +01:00
Xstar97TheNoob
0d8db3e309 add downcase to variable. 2023-02-20 09:36:24 -05:00
Xstar97TheNoob
c7647daff7 feat(ghcr.io) build container on tagged releases to ghcr.io 2023-02-20 09:25:57 -05:00
dependabot[bot]
46460dbe02 Bump ws from 8.12.0 to 8.12.1
Bumps [ws](https://github.com/websockets/ws) from 8.12.0 to 8.12.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.12.0...8.12.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-20 05:18:44 +00:00
16 changed files with 640 additions and 161 deletions

51
.github/workflows/github-image.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: GHCR Image CI
on:
push:
tags:
- "v*.*.*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: pairdrop
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -47,11 +47,11 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* On iOS and Android the devices share menu is opened instead of downloading the files
* Multiple files are transferred at once with an overall progress indicator
### Share Files Directly From Share / Context Menu
* [Share files directly from context menu on Windows](/docs/how-to.md#share-files-directly-from-context-menu-on-windows)
* [Share directly from share menu on iOS](/docs/how-to.md#share-directly-from-share-menu-on-ios)
* [Share directly from share menu on Android](/docs/how-to.md#share-directly-from-share-menu-on-android)
### Send Files or Text Directly From Share Menu, Context Menu or CLI
* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
### Other changes
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534)

View File

@@ -33,12 +33,16 @@ iOS Shortcuts to the win:
I created a simple iOS shortcut that takes your photos and saves them to your gallery:
https://routinehub.co/shortcut/13988/
### Is it possible to share files directly from the context / share menu?
Yes it finally is!
* [Share files directly from context menu on Windows](/docs/how-to.md#share-files-directly-from-context-menu-on-windows)
* [Share directly from share menu on iOS](/docs/how-to.md#share-directly-from-share-menu-on-ios)
* [Share directly from share menu on Android](/docs/how-to.md#share-directly-from-share-menu-on-android)
### Is it possible to send files or text directly from the context or share menu?
Yes, it finally is!
* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
### Is it possible to send files or text directly via CLI?
Yes, it is!
* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer.

View File

@@ -1,14 +1,22 @@
# Deployment Notes
The easiest way to get PairDrop up and running is by using Docker.
## Deployment with Docker from Docker Hub
## Deployment with Docker
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
### Image from Docker Hub
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
### Image from GHCR
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop
```
### Options / Flags
Set options by using the following flags in the `docker run` command:

View File

@@ -1,5 +1,5 @@
# How-To
## Share files directly from context menu on Windows
## Send files directly from context menu on Windows
### Registering to open files with PairDrop
The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files) is implemented
@@ -25,17 +25,58 @@ Outstandingly, it is also possible to send multiple files to PairDrop via the co
[//]: # (Todo: add screenshots)
## Share directly from share menu on iOS
## Send directly from share menu on iOS
I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu
https://routinehub.co/shortcut/13990/
[//]: # (Todo: add doku with screenshots)
## Share directly from share menu on Android
## Send directly from share menu on Android
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented but not yet tested.
When the PWA is installed, it should register itself to the share-menu of the device automatically.
Please test this feature and create an issue if it does not work.
This feature is still under development. Please test this feature and create an issue if it does not work.
## Send directly via command-line interface
Send files or text with PairDrop via command-line interface.
This opens PairDrop in the default browser where you can choose the receiver.
### Usage
```bash
$ pairdrop -h
Current domain: https://pairdrop.net/
Usage:
Open PairDrop: pairdrop
Send files: pairdrop file/directory
Send text: pairdrop -t "text"
Specify domain: pairdrop -d "https://pairdrop.net/"
Show this help text: pairdrop (-h|--help)
```
On Windows Command Prompt you need to use bash: `bash pairdrop -h`
### Setup
Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
#### Linux
1. Put file in a preferred folder e.g. `/usr/local/bin`
2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop`
3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing
`export PATH=$PATH:/opt/pairdrop-cli`
#### Mac
1. add bash file to `/usr/local/bin`
#### Windows
1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli`
2. Search for and open `Edit environment variables for your account`
3. Click `Environment Variables...`
4. Under *System Variables* select `Path` and click *Edit...*
5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed
6. Reopen Command prompt window
[< Back](/README.md)

18
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "pairdrop",
"version": "1.0.0",
"version": "1.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.0.0",
"version": "1.1.2",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33",
"unique-names-generator": "^4.3.0",
"ws": "^8.12.0"
"ws": "^8.12.1"
},
"engines": {
"node": ">=15"
@@ -633,9 +633,9 @@
}
},
"node_modules/ws": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
"engines": {
"node": ">=10.0.0"
},
@@ -1095,9 +1095,9 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"ws": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
"requires": {}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.0.0",
"version": "1.1.2",
"description": "",
"main": "index.js",
"scripts": {
@@ -14,7 +14,7 @@
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33",
"unique-names-generator": "^4.3.0",
"ws": "^8.12.0"
"ws": "^8.12.1"
},
"engines": {
"node": ">=15"

199
pairdrop-cli/pairdrop Normal file
View File

@@ -0,0 +1,199 @@
#!/bin/bash
set -e
############################################################
# Help #
############################################################
help()
{
# Display Help
echo "Send files or text with PairDrop via command-line interface."
echo "Current domain: ${DOMAIN}"
echo
echo "Usage:"
echo -e "Open PairDrop:\t\t$(basename "$0")"
echo -e "Send files:\t\t$(basename "$0") file/directory"
echo -e "Send text:\t\t$(basename "$0") -t \"text\""
echo -e "Specify domain:\t\t$(basename "$0") -d \"https://pairdrop.net/\""
echo -e "Show this help text:\t$(basename "$0") (-h|--help)"
}
openPairDrop()
{
url="$DOMAIN"
if [[ -n $params ]];then
url="${url}?${params}"
fi
if [[ -n $hash ]];then
url="${url}#${hash}"
fi
echo "PairDrop is opening at $DOMAIN"
if [[ $OS == "Windows" ]];then
start "$url"
elif [[ $OS == "Mac" ]];then
open "$url"
elif [[ $OS == "WSL" || $OS == "WSL2" ]];then
powershell.exe /c "Start-Process ${url}"
else
xdg-open "$url"
fi
exit
}
setOs()
{
unameOut=$(uname -a)
case "${unameOut}" in
*Microsoft*) OS="WSL";; #must be first since Windows subsystem for linux will have Linux in the name too
*microsoft*) OS="WSL2";; #WARNING: My v2 uses ubuntu 20.4 at the moment slightly different name may not always work
Linux*) OS="Linux";;
Darwin*) OS="Mac";;
CYGWIN*) OS="Cygwin";;
MINGW*) OS="Windows";;
*Msys) OS="Windows";;
*) OS="UNKNOWN:${unameOut}"
esac
}
specifyDomain()
{
[[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo "Incorrect format. Specify domain like https://pairdrop.net/" && exit
echo "DOMAIN=${1}" > "$CONFIGPATH"
echo -e "Domain is now set to:\n$1\n"
}
sendText()
{
params="base64text=hash"
hash=$(echo -n "${OPTARG}" | base64)
if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then
params="base64text=paste"
if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then
echo -n "$hash" | clip.exe
elif [[ $OS == "Mac" ]];then
echo -n "$hash" | pbcopy
else
(echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli"
fi
hash=
fi
openPairDrop
exit
}
sendFiles()
{
params="base64zip=hash"
if [[ $1 == */ ]]; then
path="${1::-1}"
else
path=$1
fi
zipPath="${path}_pairdrop.zip"
zipPath=${zipPath// /_}
[[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit
if [[ -d $path ]]; then
zipPathTemp="temp_${zipPath}"
[[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit
echo "Processing directory..."
# Create zip files temporarily to send directory
zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
hash=$(base64 -w 0 "$zipPathTemp")
# remove temporary temp file
rm "$zipPathTemp"
else
echo "Processing file..."
# Create zip file temporarily to send file
zip -q -b /tmp/ "$zipPath" "$path"
hash=$(base64 -w 0 "$zipPath")
fi
# remove temporary temp file
rm "$zipPath"
if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then
params="base64zip=paste"
if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then
echo -n "$hash" | clip.exe
elif [[ $OS == "Mac" ]];then
echo -n "$hash" | pbcopy
else
(echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli"
fi
hash=
fi
openPairDrop
exit
}
############################################################
############################################################
# Main program #
############################################################
############################################################
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
pushd . > '/dev/null';
SCRIPTPATH="${BASH_SOURCE[0]:-$0}";
while [ -h "$SCRIPTPATH" ];
do
cd "$( dirname -- "$SCRIPTPATH"; )";
SCRIPTPATH="$( readlink -f -- "$SCRIPTPATH"; )";
done
cd "$( dirname -- "$SCRIPTPATH"; )" > '/dev/null';
SCRIPTPATH="$( pwd; )";
popd > '/dev/null';
CONFIGPATH="${SCRIPTPATH}/.pairdrop-cli-config"
[ ! -f "$CONFIGPATH" ] &&
specifyDomain "https://pairdrop.net/" &&
[ ! -f "$CONFIGPATH" ] &&
echo "Could not create config file. Add 'DOMAIN=https://pairdrop.net/' to a file called .pairdrop-cli-config in the same file as this 'pairdrop' bash file"
[ ! -f "$CONFIGPATH" ] || export "$(grep -v '^#' "$CONFIGPATH" | xargs)"
setOs
############################################################
# Process the input options. Add options as needed. #
############################################################
# Get the options
# open PairDrop if no options are given
[[ $# -eq 0 ]] && openPairDrop && exit
# display help and exit if first argument is "--help" or more than 2 arguments are given
[ "$1" == "--help" ] || [[ $# -gt 2 ]] && help && exit
while getopts "d:ht:*" option; do
case $option in
d) # specify domain
specifyDomain "$2"
exit;;
t) # Send text
sendText
exit;;
h | ?) # display help and exit
help
exit;;
esac
done
# Send file(s)
# display help and exit if 2 arguments are given or if file does not exist
[[ $# -eq 2 ]] || [[ ! -a $1 ]] && help && exit
sendFiles "$1"

View File

@@ -213,11 +213,11 @@
</x-paper>
</x-background>
</x-dialog>
<!-- base64ZipDialog Dialog -->
<x-dialog id="base64ZipDialog">
<!-- base64PasteDialog Dialog -->
<x-dialog id="base64PasteDialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64ZipPasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" close>Close</button>
</x-paper>
</x-background>

View File

@@ -143,7 +143,7 @@ class PeersUI {
descriptor = `${files[0].name} and ${files.length-1} other files`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} else {
descriptor = "pasted text";
descriptor = "shared text";
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
}
@@ -590,24 +590,32 @@ class ReceiveFileDialog extends ReceiveDialog {
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continueCallback = async _ => {
navigator.share({
files: files
}).catch(err => console.error(err));
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download";
this.$shareOrDownloadBtn.download = filenameDownload;
this.$shareOrDownloadBtn.href = url;
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.$shareOrDownloadBtn.click();
this.continue();
}).catch(r => console.error(r));
}
@@ -831,8 +839,8 @@ class PairDeviceDialog extends Dialog {
// Display the QR code for the url
const qr = new QRCode({
content: this._getShareRoomURL(),
width: 80,
height: 80,
width: 150,
height: 150,
padding: 0,
background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
@@ -1081,67 +1089,135 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog {
constructor() {
super('base64ZipDialog');
super('base64PasteDialog');
const urlParams = new URL(window.location).searchParams;
const base64Zip = urlParams.get('base64zip');
const base64Text = urlParams.get('base64text');
this.$pasteBtn = this.$el.querySelector('#base64ZipPasteBtn')
this.$pasteBtn.addEventListener('click', _ => this.processClipboard())
const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
if (base64Text) {
this.processBase64Text(base64Text);
} else if (base64Zip) {
if (!navigator.clipboard.readText) {
setTimeout(_ => Events.fire('notify-user', 'This feature is not available on your device.'), 500);
this.clearBrowserHistory();
return;
}
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
}).finally(_ => {
this.hide();
});
} else {
// ?base64text=BASE64ENCODED
// base64 encoded string was part of url param (not recommended)
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
}).finally(_ => {
this.hide();
});
}
} else if (base64Zip) {
this.show();
if (base64Zip === "hash") {
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
}
}
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
}
}
processBase64Text(base64Text){
try {
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
let decodedText = decodeURIComponent(escape(window.atob(base64Text)));
Events.fire('activate-paste-mode', {files: [], text: decodedText});
} catch (e) {
setTimeout(_ => Events.fire('notify-user', 'Content incorrect.'), 500);
} finally {
this.clearBrowserHistory();
this.hide();
}
resolve();
});
}
async processClipboard() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
try {
const base64zip = await navigator.clipboard.readText();
let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
Events.fire('activate-paste-mode', {files: files, text: ""})
} catch (e) {
Events.fire('notify-user', 'Clipboard content is incorrect.')
} finally {
this.clearBrowserHistory();
this.hide();
async processBase64Zip(base64zip) {
this._setPasteBtnToProcessing();
let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
Events.fire('activate-paste-mode', {files: files, text: ""});
}
clearBrowserHistory() {
window.history.replaceState({}, "Rewrite URL", '/');
}
hide() {
this.clearBrowserHistory();
super.hide();
}
}
class Toast extends Dialog {

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.0.0';
const cacheVersion = 'v1.1.2';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -11,7 +11,8 @@
/* Layout */
html {
height: 100%;
min-height: 100%;
height: -webkit-fill-available;
}
html,
@@ -25,6 +26,8 @@ body {
}
body {
min-height: 100%;
min-height: -webkit-fill-available;
flex-grow: 1;
align-items: center;
justify-content: center;
@@ -407,6 +410,7 @@ x-dialog x-background {
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
overflow: overlay;
}
x-dialog x-paper {
@@ -421,6 +425,13 @@ x-dialog x-paper {
will-change: transform;
}
#pairDeviceDialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -491,8 +502,8 @@ x-dialog .font-subheading {
#roomKeyQrCode {
padding: inherit;
margin: auto;
width: 80px;
height: 80px;
width: 150px;
height: 150px;
}
#pairDeviceDialog hr {
@@ -605,21 +616,22 @@ x-dialog .row-reverse {
margin-bottom: 25px;
}
#base64ZipPasteBtn {
#base64PasteBtn {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
}
#base64ZipDialog button {
#base64PasteDialog button {
margin: auto;
border-radius: 8px;
}
#base64ZipDialog button[close] {
#base64PasteDialog button[close] {
margin-top: 20px;
}
#base64ZipDialog button[close]:before {
#base64PasteDialog button[close]:before {
border-radius: 8px;
}
@@ -931,7 +943,7 @@ screen and (min-width: 1100px) {
position: fixed;
}
x-instructions:before {
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(mobile);
}
}

View File

@@ -216,11 +216,11 @@
</x-paper>
</x-background>
</x-dialog>
<!-- base64ZipDialog Dialog -->
<x-dialog id="base64ZipDialog">
<!-- base64PasteDialog Dialog -->
<x-dialog id="base64PasteDialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64ZipPasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" close>Close</button>
</x-paper>
</x-background>

View File

@@ -143,7 +143,7 @@ class PeersUI {
descriptor = `${files[0].name} and ${files.length-1} other files`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} else {
descriptor = "pasted text";
descriptor = "shared text";
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
}
@@ -591,24 +591,32 @@ class ReceiveFileDialog extends ReceiveDialog {
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continueCallback = async _ => {
navigator.share({
files: files
}).catch(err => console.error(err));
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download";
this.$shareOrDownloadBtn.download = filenameDownload;
this.$shareOrDownloadBtn.href = url;
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.$shareOrDownloadBtn.click();
this.continue();
}).catch(r => console.error(r));
}
@@ -832,8 +840,8 @@ class PairDeviceDialog extends Dialog {
// Display the QR code for the url
const qr = new QRCode({
content: this._getShareRoomURL(),
width: 80,
height: 80,
width: 150,
height: 150,
padding: 0,
background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
@@ -1082,67 +1090,135 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog {
constructor() {
super('base64ZipDialog');
super('base64PasteDialog');
const urlParams = new URL(window.location).searchParams;
const base64Zip = urlParams.get('base64zip');
const base64Text = urlParams.get('base64text');
this.$pasteBtn = this.$el.querySelector('#base64ZipPasteBtn')
this.$pasteBtn.addEventListener('click', _ => this.processClipboard())
const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
if (base64Text) {
this.processBase64Text(base64Text);
} else if (base64Zip) {
if (!navigator.clipboard.readText) {
setTimeout(_ => Events.fire('notify-user', 'This feature is not available on your device.'), 500);
this.clearBrowserHistory();
return;
}
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
}).finally(_ => {
this.hide();
});
} else {
// ?base64text=BASE64ENCODED
// base64 encoded string was part of url param (not recommended)
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
}).finally(_ => {
this.hide();
});
}
} else if (base64Zip) {
this.show();
if (base64Zip === "hash") {
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
}
}
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
}
}
processBase64Text(base64Text){
try {
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
let decodedText = decodeURIComponent(escape(window.atob(base64Text)));
Events.fire('activate-paste-mode', {files: [], text: decodedText});
} catch (e) {
setTimeout(_ => Events.fire('notify-user', 'Content incorrect.'), 500);
} finally {
this.clearBrowserHistory();
this.hide();
}
resolve();
});
}
async processClipboard() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
try {
const base64zip = await navigator.clipboard.readText();
let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
Events.fire('activate-paste-mode', {files: files, text: ""})
} catch (e) {
Events.fire('notify-user', 'Clipboard content is incorrect.')
} finally {
this.clearBrowserHistory();
this.hide();
async processBase64Zip(base64zip) {
this._setPasteBtnToProcessing();
let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
Events.fire('activate-paste-mode', {files: files, text: ""});
}
clearBrowserHistory() {
window.history.replaceState({}, "Rewrite URL", '/');
}
hide() {
this.clearBrowserHistory();
super.hide();
}
}
class Toast extends Dialog {

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.0.0';
const cacheVersion = 'v1.1.2';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -12,7 +12,8 @@
/* Layout */
html {
height: 100%;
min-height: 100%;
height: -webkit-fill-available;
}
html,
@@ -26,6 +27,8 @@ body {
}
body {
min-height: 100%;
min-height: -webkit-fill-available;
flex-grow: 1;
align-items: center;
justify-content: center;
@@ -416,6 +419,7 @@ x-dialog x-background {
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
overflow: overlay;
}
x-dialog x-paper {
@@ -430,6 +434,13 @@ x-dialog x-paper {
will-change: transform;
}
#pairDeviceDialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -500,8 +511,8 @@ x-dialog .font-subheading {
#roomKeyQrCode {
padding: inherit;
margin: auto;
width: 80px;
height: 80px;
width: 150px;
height: 150px;
}
#pairDeviceDialog hr {
@@ -614,21 +625,22 @@ x-dialog .row-reverse {
margin-bottom: 25px;
}
#base64ZipPasteBtn {
#base64PasteBtn {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
}
#base64ZipDialog button {
#base64PasteDialog button {
margin: auto;
border-radius: 8px;
}
#base64ZipDialog button[close] {
#base64PasteDialog button[close] {
margin-top: 20px;
}
#base64ZipDialog button[close]:before {
#base64PasteDialog button[close]:before {
border-radius: 8px;
}
@@ -944,7 +956,7 @@ screen and (min-width: 1100px) {
position: fixed;
}
x-instructions:before {
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(mobile);
}
}