diff --git a/pairdrop b/pairdrop new file mode 100644 index 0000000..baebfd8 --- /dev/null +++ b/pairdrop @@ -0,0 +1,123 @@ +#!/bin/bash +set -e + +############################################################ +# Help # +############################################################ +help() +{ + # Display Help + echo "Send files or text via PairDrop via commandline." + echo + echo -e "To send files:\t$(basename "$0") file" + echo -e "To send text:\t$(basename "$0") -t \"text\"" +} + +openPairDrop() +{ + # openPairDrop + url=$domain + if [[ -n $params ]];then + url=$url"?"$params + fi + if [[ -n $hash ]];then + url=$url"#"$hash + fi + echo "PairDrop is opening in a browser." + + if [[ $OS == "Windows" ]];then + start "$url" + elif [[ $OS == "Mac" ]];then + open "$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 +} + +############################################################ +############################################################ +# Main program # +############################################################ +############################################################ +domain="https://pairdrop.net/" +domain="https://192.168.2.90:8443/" +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 "ht:*" option; do + case $option in + t) # Send text + 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;; + 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 +params="base64zip=hash" +if [[ -d $1 ]]; then + zipPath="${1::-1}_pairdrop_temp.zip" + [[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit + zip -r -q -b /tmp/ "$zipPath" "$1" + hash=$(zip -q -b /tmp/ - "$zipPath" | base64 -w 0) + rm "$zipPath" +else + hash=$(zip -q -b /tmp/ - "$1" | base64 -w 0) +fi + +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 diff --git a/public/index.html b/public/index.html index 08eed7d..14d50cf 100644 --- a/public/index.html +++ b/public/index.html @@ -213,11 +213,11 @@ - - + + - + diff --git a/public/scripts/ui.js b/public/scripts/ui.js index ce484e4..1d38868 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -143,7 +143,7 @@ class PeersUI { descriptor = `${files[0].name} and ${files.length-1} other files`; noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; } else { - descriptor = "pasted text"; + descriptor = "shared text"; noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; } @@ -1081,67 +1081,129 @@ 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.'); + }).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.'); + }).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.'); + }).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 device.'); + 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.'); + }).finally(_ => { + this.hide(); + }); + } else { + this.processBase64Zip(base64) + .catch(_ => { + Events.fire('notify-user', '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 { diff --git a/public/styles.css b/public/styles.css index 7497b77..dcec033 100644 --- a/public/styles.css +++ b/public/styles.css @@ -605,21 +605,21 @@ 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; } diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 534d06d..bc7bc22 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -216,11 +216,11 @@
- - + + - + diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index ba48292..e7dd79d 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -143,7 +143,7 @@ class PeersUI { descriptor = `${files[0].name} and ${files.length-1} other files`; noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; } else { - descriptor = "pasted text"; + descriptor = "shared text"; noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; } @@ -1082,67 +1082,129 @@ 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.'); + }).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.'); + }).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.'); + }).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 device.'); + 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.'); + }).finally(_ => { + this.hide(); + }); + } else { + this.processBase64Zip(base64) + .catch(_ => { + Events.fire('notify-user', '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 { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 8d3c5e0..9df5852 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -614,21 +614,21 @@ 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; }