diff --git a/public/index.html b/public/index.html index 7db7f4c..a07981d 100644 --- a/public/index.html +++ b/public/index.html @@ -521,8 +521,13 @@ - - +
+

+
+
+ + +
diff --git a/public/lang/en.json b/public/lang/en.json index 9161e41..f7bc40f 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -80,9 +80,11 @@ "send": "Send", "receive-text-title": "Message Received", "copy": "Copy", + "base64-title-files": "Share Files", + "base64-title-text": "Share Text", "base64-processing": "Processing…", - "base64-tap-to-paste": "Tap here to paste {{type}}", - "base64-paste-to-send": "Paste here to send {{type}}", + "base64-tap-to-paste": "Tap here to share {{type}}", + "base64-paste-to-send": "Paste clipboard here to share {{type}}", "base64-text": "text", "base64-files": "files", "file-other-description-image": "and 1 other image", diff --git a/public/scripts/main.js b/public/scripts/main.js index 5febe95..aeaac43 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -59,6 +59,9 @@ class PairDrop { await this.hydrate(); console.log("UI hydrated."); + + // Evaluate url params as soon as ws is connected + Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true}); } registerServiceWorker() { @@ -171,6 +174,44 @@ class PairDrop { this.server = new ServerConnection(); this.peers = new PeersManager(this.server); } + + async evaluateUrlParams() { + // get url params + const urlParams = new URLSearchParams(window.location.search); + const hash = window.location.hash.substring(1); + + // evaluate url params + if (urlParams.has('pair_key')) { + const pairKey = urlParams.get('pair_key'); + this.pairDeviceDialog._pairDeviceJoin(pairKey); + } + else if (urlParams.has('room_id')) { + const roomId = urlParams.get('room_id'); + this.publicRoomDialog._joinPublicRoom(roomId); + } + else if (urlParams.has('base64text')) { + const base64Text = urlParams.get('base64text'); + await this.base64Dialog.evaluateBase64Text(base64Text, hash); + } + else if (urlParams.has('base64zip')) { + const base64Zip = urlParams.get('base64zip'); + await this.base64Dialog.evaluateBase64Zip(base64Zip, hash); + } + else if (urlParams.has("share_target")) { + const shareTargetType = urlParams.get("share_target"); + const title = urlParams.get('title') || ''; + const text = urlParams.get('text') || ''; + const url = urlParams.get('url') || ''; + await this.webShareTargetUI.evaluateShareTarget(shareTargetType, title, text, url); + } + else if (urlParams.has("file_handler")) { + await this.webFileHandlersUI.evaluateLaunchQueue(); + } + + // remove url params from url + const urlWithoutParams = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", urlWithoutParams); + } } const pairDrop = new PairDrop(); \ No newline at end of file diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 3039058..7a105c8 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1321,8 +1321,6 @@ class PairDeviceDialog extends Dialog { this.$el.addEventListener('paste', e => this._onPaste(e)); this.$qrCode.addEventListener('click', _ => this._copyPairUrl()); - this.evaluateUrlAttributes(); - this.pairPeer = {}; } @@ -1344,15 +1342,6 @@ class PairDeviceDialog extends Dialog { this.inputKeyContainer._onPaste(pastedKey); } - evaluateUrlAttributes() { - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.has('pair_key')) { - this._pairDeviceJoin(urlParams.get('pair_key')); - const url = getUrlWithoutArguments(); - window.history.replaceState({}, "Rewrite URL", url); //remove pair_key from url - } - } - _pairDeviceInitiate() { Events.fire('pair-device-initiate'); } @@ -1700,8 +1689,6 @@ class PublicRoomDialog extends Dialog { this.$el.addEventListener('paste', e => this._onPaste(e)); this.$qrCode.addEventListener('click', _ => this._copyShareRoomUrl()); - this.evaluateUrlAttributes(); - Events.on('ws-connected', _ => this._onWsConnected()); Events.on('translation-loaded', _ => this.setFooterBadge()); } @@ -1791,15 +1778,6 @@ class PublicRoomDialog extends Dialog { }) } - evaluateUrlAttributes() { - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.has('room_id')) { - this._joinPublicRoom(urlParams.get('room_id')); - const url = getUrlWithoutArguments(); - window.history.replaceState({}, "Rewrite URL", url); //remove pair_key from url - } - } - _onWsConnected() { let roomId = sessionStorage.getItem('public_room_id'); @@ -2147,61 +2125,47 @@ class Base64Dialog extends Dialog { constructor() { super('base64-paste-dialog'); - const urlParams = new URL(window.location).searchParams; - const base64Text = urlParams.get('base64text'); - const base64Zip = urlParams.get('base64zip'); - const base64Hash = window.location.hash.substring(1); + this.$title = this.$el.querySelector('.dialog-title'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$fallbackTextarea = this.$el.querySelector('.textarea'); + } - if (base64Text) { + async evaluateBase64Text(base64Text, hash) { + this.$title.innerText = Localization.getTranslation('dialogs.base64-title-text'); + + if (base64Text === 'paste') { + // ?base64text=paste + // base64 encoded string is ready to be pasted from clipboard + this.preparePasting('text'); this.show(); - if (base64Text === 'paste') { - // ?base64text=paste - // base64 encoded string is ready to be pasted from clipboard - this.preparePasting('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', Localization.getTranslation("notifications.text-content-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', Localization.getTranslation("notifications.text-content-incorrect")); - console.log("Text content incorrect."); - }).finally(() => { - this.hide(); - }); - } } - else if (base64Zip) { + else if (base64Text === 'hash') { + // ?base64text=hash#BASE64ENCODED + // base64 encoded text is url hash which cannot be seen by the server and is faster (recommended) 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', Localization.getTranslation("notifications.file-content-incorrect")); - console.log("File content incorrect."); - }).finally(() => { - this.hide(); - }); - } - else { - // ?base64zip=paste || ?base64zip=true - this.preparePasting('files'); - } + await this.processBase64Text(hash) + } + else { + // ?base64text=BASE64ENCODED + // base64 encoded text is part of the url param. Seen by server and slow (not recommended) + this.show(); + await this.processBase64Text(base64Text) + } + } + + async evaluateBase64Zip(base64Zip, hash) { + this.$title.innerText = Localization.getTranslation('dialogs.base64-title-files'); + + if (base64Zip === 'paste') { + // ?base64zip=paste || ?base64zip=true + this.preparePasting('files'); + this.show(); + } + else if (base64Zip === 'hash') { + // ?base64zip=hash#BASE64ENCODED + // base64 encoded zip file is url hash which cannot be seen by the server + await this.processBase64Zip(hash) } } @@ -2234,28 +2198,15 @@ class Base64Dialog extends Dialog { async processInput(type) { const base64 = this.$fallbackTextarea.textContent; this.$fallbackTextarea.textContent = ''; - await this.processBase64(type, base64); + await this.processPastedBase64(type, base64); } async processClipboard(type) { const base64 = await navigator.clipboard.readText(); - await this.processBase64(type, base64); + await this.processPastedBase64(type, base64); } - isValidBase64(base64) { - try { - // check if input is base64 encoded - window.atob(base64); - return true; - } catch (e) { - // input is not base64 string. - return false; - } - } - - async processBase64(type, base64) { - if (!base64 || !this.isValidBase64(base64)) return; - this._setPasteBtnToProcessing(); + async processPastedBase64(type, base64) { try { if (type === 'text') { await this.processBase64Text(base64); @@ -2263,51 +2214,50 @@ class Base64Dialog extends Dialog { else { await this.processBase64Zip(base64); } - } catch(_) { + } + catch(e) { Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); console.log("Clipboard content is incorrect.") } this.hide(); } - processBase64Text(base64Text){ - return new Promise((resolve) => { - this._setPasteBtnToProcessing(); - let decodedText = decodeURIComponent(escape(window.atob(base64Text))); + async processBase64Text(base64){ + this._setPasteBtnToProcessing(); + + try { + const decodedText = await decodeBase64Text(base64); if (ShareTextDialog.isApproveShareTextSet()) { Events.fire('share-text-dialog', decodedText); - } else { + } + else { Events.fire('activate-share-mode', {text: decodedText}); } - resolve(); - }); + } + catch (e) { + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); + console.log("Text content incorrect."); + } + + this.hide(); } - async processBase64Zip(base64zip) { + async processBase64Zip(base64) { this._setPasteBtnToProcessing(); - let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n); - while (n--) { - u8arr[n] = bstr.charCodeAt(n); + + try { + const decodedFiles = await decodeBase64Files(base64); + Events.fire('activate-share-mode', {files: decodedFiles}); + } + catch (e) { + Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect")); + console.log("File content incorrect."); } - 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-share-mode', {files: files}); - } - - clearBrowserHistory() { - const url = getUrlWithoutArguments(); - window.history.replaceState({}, "Rewrite URL", url); + this.hide(); } hide() { - this.clearBrowserHistory(); this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); super.hide(); @@ -2529,80 +2479,72 @@ class NetworkStatusUI { } class WebShareTargetUI { - constructor() { - const urlParams = new URL(window.location).searchParams; - const share_target_type = urlParams.get("share-target") - if (share_target_type) { - if (share_target_type === "text") { - const title = urlParams.get('title') || ''; - const text = urlParams.get('text') || ''; - const url = urlParams.get('url') || ''; - let shareTargetText; - if (url) { - shareTargetText = url; // we share only the link - no text. - } - else if (title && text) { - shareTargetText = title + '\r\n' + text; - } - else { - shareTargetText = title + text; - } - - if (ShareTextDialog.isApproveShareTextSet()) { - Events.fire('share-text-dialog', shareTargetText); - } else { - Events.fire('activate-share-mode', {text: shareTargetText}); - } + async evaluateShareTarget(shareTargetType, title, text, url) { + if (shareTargetType === "text") { + let shareTargetText; + if (url) { + shareTargetText = url; // we share only the link - no text. + } + else if (title && text) { + shareTargetText = title + '\r\n' + text; + } + else { + shareTargetText = title + text; } - else if (share_target_type === "files") { - let openRequest = window.indexedDB.open('pairdrop_store') - openRequest.onsuccess = e => { - const db = e.target.result; - const tx = db.transaction('share_target_files', 'readwrite'); - const store = tx.objectStore('share_target_files'); - const request = store.getAll(); - request.onsuccess = _ => { - const fileObjects = request.result; - let filesReceived = []; - for (let i=0; i db.close(); - Events.fire('activate-share-mode', {files: filesReceived}) + if (ShareTextDialog.isApproveShareTextSet()) { + Events.fire('share-text-dialog', shareTargetText); + } + else { + Events.fire('activate-share-mode', {text: shareTargetText}); + } + } + else if (shareTargetType === "files") { + let openRequest = window.indexedDB.open('pairdrop_store') + openRequest.onsuccess = e => { + const db = e.target.result; + const tx = db.transaction('share_target_files', 'readwrite'); + const store = tx.objectStore('share_target_files'); + const request = store.getAll(); + request.onsuccess = _ => { + const fileObjects = request.result; + + let filesReceived = []; + for (let i = 0; i < fileObjects.length; i++) { + filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name)); } + + const clearRequest = store.clear() + clearRequest.onsuccess = _ => db.close(); + + Events.fire('activate-share-mode', {files: filesReceived}) } } - const url = getUrlWithoutArguments(); - window.history.replaceState({}, "Rewrite URL", url); } } } class WebFileHandlersUI { - constructor() { - const urlParams = new URL(window.location).searchParams; - if (urlParams.has("file_handler") && "launchQueue" in window) { - launchQueue.setConsumer(async launchParams => { - console.log("Launched with: ", launchParams); - if (!launchParams.files.length) - return; - let files = []; + async evaluateLaunchQueue() { + if (!"launchQueue" in window) return; - for (let i=0; i { + console.log("Launched with: ", launchParams); + + if (!launchParams.files.length) return; + + let files = []; + + for (let i = 0; i < launchParams.files.length; i++) { + if (i !== 0 && await launchParams.files[i].isSameEntry(launchParams.files[i-1])) continue; + + const file = await launchParams.files[i].getFile(); + files.push(file); + } + + Events.fire('activate-share-mode', {files: files}) + }); } } diff --git a/public/scripts/util.js b/public/scripts/util.js index ad86924..4eeb744 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -504,4 +504,29 @@ function getResizedImageDataUrl(file, width = undefined, height = undefined, qua } image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`); }) +} + +async function decodeBase64Files(base64) { + if (!base64) throw new Error('Base64 is empty'); + + let bstr = atob(base64), 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)); + } + return files +} + +async function decodeBase64Text(base64) { + if (!base64) throw new Error('Base64 is empty'); + + return decodeURIComponent(escape(window.atob(base64))) } \ No newline at end of file diff --git a/public/styles/deferred-styles.css b/public/styles/deferred-styles.css index d9d90ad..97d483e 100644 --- a/public/styles/deferred-styles.css +++ b/public/styles/deferred-styles.css @@ -705,6 +705,7 @@ x-dialog .dialog-subheader { width: 100%; height: 40vh; border: solid 12px #438cff; + margin: 6px; } #base64-paste-dialog .textarea {