Merge pull request #39 from schlagmichdoch/enable_sending_from_cli

Closes #34
This commit is contained in:
schlagmichdoch 2023-02-22 02:37:18 +01:00 committed by GitHub
commit 12a2fc1b0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 494 additions and 114 deletions

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,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)

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}`;
}
@ -1081,67 +1081,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

@ -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;
}

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}`;
}
@ -1082,67 +1082,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

@ -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;
}