diff --git a/public/index.html b/public/index.html
index d67b765..87b2372 100644
--- a/public/index.html
+++ b/public/index.html
@@ -447,7 +447,7 @@
-
+
diff --git a/public/scripts/ui.js b/public/scripts/ui.js
index 8bc39c7..63434b5 100644
--- a/public/scripts/ui.js
+++ b/public/scripts/ui.js
@@ -939,26 +939,28 @@ class ReceiveDialog extends Dialog {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {
let fileOther = "";
-
if (files.length === 2) {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image")
: Localization.getTranslation("dialogs.file-other-description-file");
}
- else if (files.length >= 2) {
+ else if (files.length > 2) {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
}
- this.$fileOther.innerText = fileOther;
-
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
- this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
+ const fileStem = fileName.substring(0, fileName.length - fileExtension.length);
+
+ const fileSize = this._formatFileSize(totalSize);
+
+ this.$fileOther.innerText = fileOther;
+ this.$fileStem.innerText = fileStem;
this.$fileExtension.innerText = fileExtension;
- this.$fileSize.innerText = this._formatFileSize(totalSize);
+ this.$fileSize.innerText = fileSize;
this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$displayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
@@ -1005,6 +1007,55 @@ class ReceiveFileDialog extends ReceiveDialog {
await this._displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
}
+ canShareFilesViaMenu(files) {
+ return window.isMobile && !!navigator.share && navigator.canShare({files});
+ }
+
+ async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {
+ const descriptor = this._getDescriptor(files, imagesOnly);
+ const documentTitleTranslation = files.length === 1
+ ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
+ : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
+
+ // If possible, share via menu - else download files
+ const shareViaMenu = this.canShareFilesViaMenu(files);
+
+ this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
+ this._setTitle(descriptor);
+
+ await this._addFileToPreviewBox(files[0]);
+
+ document.title = documentTitleTranslation;
+ changeFavicon("images/favicon-96x96-notification.png");
+
+ if (shareViaMenu) {
+ await this._setViaShareMenu(files);
+ }
+ else {
+ await this._setViaDownload(peerId, files, totalSize, descriptor);
+ }
+
+ Events.fire('set-progress', {peerId: peerId, progress: 1, status: null});
+ }
+
+ _getDescriptor(files, imagesOnly) {
+ let descriptor;
+ if (files.length === 1) {
+ descriptor = imagesOnly
+ ? Localization.getTranslation("dialogs.title-image")
+ : Localization.getTranslation("dialogs.title-file");
+ }
+ else {
+ descriptor = imagesOnly
+ ? Localization.getTranslation("dialogs.title-image-plural")
+ : Localization.getTranslation("dialogs.title-file-plural");
+ }
+ return descriptor;
+ }
+
+ _setTitle(descriptor) {
+ this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
+ }
createPreviewElement(file) {
return new Promise((resolve, reject) => {
try {
@@ -1016,163 +1067,240 @@ class ReceiveFileDialog extends ReceiveDialog {
}
if (Object.keys(previewElement).indexOf(mime) === -1) {
- resolve(false);
- }
- else {
- let element = document.createElement(previewElement[mime]);
- element.controls = true;
- element.onload = _ => {
- this.$previewBox.appendChild(element);
- resolve(true);
- };
- element.onloadeddata = _ => {
- this.$previewBox.appendChild(element);
- resolve(true);
- };
- element.onerror = _ => {
- reject(`${mime} preview could not be loaded from type ${file.type}`);
- };
- element.src = URL.createObjectURL(file);
+ reject('Preview is only supported for images, audio and video');
+ return;
}
+
+ let element = document.createElement(previewElement[mime]);
+ let timeout = setTimeout(_ => {
+ reject('Preview could not be loaded: timeout', file.type);
+ }, 1000);
+
+ element.controls = true;
+ element.onload = _ => {
+ clearTimeout(timeout);
+ resolve(element);
+ };
+ element.onloadeddata = _ => {
+ clearTimeout(timeout);
+ resolve(element);
+ };
+ element.onerror = _ => {
+ clearTimeout(timeout);
+ reject('Preview could not be loaded from type', file.type);
+ };
+ element.src = URL.createObjectURL(file);
} catch (e) {
- reject(`preview could not be loaded from type ${file.type}`);
+ reject('Preview could not be loaded from type', file.type);
}
});
}
- async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {
- this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
-
- let descriptor, url, filenameDownload;
- if (files.length === 1) {
- descriptor = imagesOnly
- ? Localization.getTranslation("dialogs.title-image")
- : Localization.getTranslation("dialogs.title-file");
- }
- else {
- descriptor = imagesOnly
- ? Localization.getTranslation("dialogs.title-image-plural")
- : Localization.getTranslation("dialogs.title-file-plural");
- }
- this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
-
- const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
- if (canShare) {
- this.$shareBtn.removeAttribute('hidden');
- this.$shareBtn.onclick = _ => {
- navigator.share({files: files})
- .catch(err => {
- Logger.error(err);
- });
- }
- }
-
- let downloadZipped = false;
- if (files.length > 1) {
- downloadZipped = true;
- try {
- let bytesCompleted = 0;
- zipper.createNewZipWriter();
- for (let i=0; i {
- Events.fire('set-progress', {
- peerId: peerId,
- progress: (bytesCompleted + progress) / totalSize,
- status: 'process'
- })
- }
- });
- bytesCompleted += files[i].size;
- }
- url = await zipper.getBlobURL();
-
- let now = new Date(Date.now());
- let year = now.getFullYear().toString();
- let month = (now.getMonth()+1).toString();
- month = month.length < 2 ? "0" + month : month;
- let date = now.getDate().toString();
- date = date.length < 2 ? "0" + date : date;
- let hours = now.getHours().toString();
- hours = hours.length < 2 ? "0" + hours : hours;
- let minutes = now.getMinutes().toString();
- minutes = minutes.length < 2 ? "0" + minutes : minutes;
- filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
- } catch (e) {
- Logger.error(e);
- downloadZipped = false;
- }
- }
-
- this.$downloadBtn.removeAttribute('disabled');
- this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
- this.$downloadBtn.onclick = _ => {
- if (downloadZipped) {
- let tmpZipBtn = document.createElement("a");
- tmpZipBtn.download = filenameDownload;
- tmpZipBtn.href = url;
- tmpZipBtn.click();
- }
- else {
- this._downloadFilesIndividually(files);
- }
-
- if (!canShare) {
- this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again");
- }
- Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor}));
-
- // Prevent clicking the button multiple times
- this.$downloadBtn.style.pointerEvents = "none";
- setTimeout(() => this.$downloadBtn.style.pointerEvents = "unset", 2000);
- };
-
- document.title = files.length === 1
- ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
- : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
- changeFavicon("images/favicon-96x96-notification.png");
-
- Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
- this.show();
+ _disableButton($button, duration) {
+ $button.style.pointerEvents = "none";
setTimeout(() => {
- // wait for the dialog to be shown
- if (canShare) {
- this.$shareBtn.click();
- }
- else {
- this.$downloadBtn.click();
- }
- }, 500);
-
- this.createPreviewElement(files[0])
- .then(canPreview => {
- if (canPreview) {
- Logger.debug('the file is able to preview');
- }
- else {
- Logger.debug('the file is not able to preview');
- }
- })
- .catch(r => console.error(r));
+ $button.style.pointerEvents = "unset";
+ }, duration);
}
- _downloadFilesIndividually(files) {
+ async _setShareButton(files) {
+ this.$shareBtn.onclick = _ => {
+ navigator.share({files: files})
+ .catch(err => {
+ Logger.error(err);
+ });
+
+ // Prevent clicking the button multiple times
+ this._disableButton(this.$shareBtn, 2000);
+ }
+ this.$shareBtn.removeAttribute('disabled');
+ this.$shareBtn.removeAttribute('hidden');
+ }
+
+ async _setDownloadButton(peerId, files, totalSize, descriptor) {
+ let downloadTranslation = Localization.getTranslation("dialogs.download")
+ let downloadSuccessfulTranslation = Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor});
+
+ this.$downloadBtn.innerText = downloadTranslation;
+ this.$downloadBtn.removeAttribute('disabled');
+ this.$downloadBtn.removeAttribute('hidden');
+
+ let zipFileUrl, zipFileName;
+
+ const tooBigToZip = window.iOS && totalSize > 256000000;
+ this.sendAsZip = false;
+ if (files.length > 1 && !tooBigToZip) {
+ zipFileName = this._createZipFilename()
+ zipFileUrl = await this._createZipFile(files, zipProgress => {
+ Events.fire('set-progress', {
+ peerId: peerId,
+ progress: zipProgress / totalSize,
+ status: 'process'
+ })
+ })
+
+ this.sendAsZip = !!zipFileUrl;
+ }
+
+ // If single file or zipping failed -> download files individually -> else download zip
+ if (this.sendAsZip) {
+ this._setDownloadButtonToZip(zipFileUrl, zipFileName, downloadSuccessfulTranslation);
+ } else {
+ this._setDownloadButtonToFiles(files, downloadSuccessfulTranslation, downloadTranslation);
+ }
+ }
+
+ _setDownloadButtonToZip(zipFileUrl, zipFileName, downloadSuccessfulTranslation) {
+ this.downloadSuccessful = false;
+ this.$downloadBtn.onclick = _ => {
+ this._downloadFileFromUrl(zipFileUrl, zipFileName)
+
+ Events.fire('notify-user', downloadSuccessfulTranslation);
+ this.downloadSuccessful = true;
+
+ this.hide();
+ };
+ }
+
+ _setDownloadButtonToFiles(files, downloadSuccessfulTranslation, downloadTranslation) {
+ this.downloadSuccessful = false;
+ let i = 0;
+
+ this.$downloadBtn.innerText = `${downloadTranslation} ${i + 1}/${files.length}`;
+
+ this.$downloadBtn.onclick = _ => {
+ this._disableButton(this.$shareBtn, 2000);
+
+ this._downloadFiles([files[i]]);
+
+ if (i < files.length - 1) {
+ i++;
+ this.$downloadBtn.innerText = `${downloadTranslation} ${i + 1}/${files.length}`;
+ return
+ }
+
+ Events.fire('notify-user', downloadSuccessfulTranslation);
+ this.downloadSuccessful = true;
+
+ this.hide()
+ };
+ }
+
+ async _addFileToPreviewBox(file) {
+ try {
+ const previewElement = await this.createPreviewElement(file)
+ this.$previewBox.appendChild(previewElement);
+ }
+ catch (e) {
+ Logger.log(e);
+ }
+ }
+
+ _downloadFileFromUrl(url, name) {
+ let tmpZipBtn = document.createElement("a");
+ tmpZipBtn.download = name;
+ tmpZipBtn.href = url;
+ tmpZipBtn.click();
+ }
+
+ _downloadFiles(files) {
let tmpBtn = document.createElement("a");
- for (let i=0; i onProgressCallback(bytesCompleted + progress)
+ });
+ bytesCompleted += files[i].size;
+ }
+
+ return await zipper.getBlobURL();
+ }
+ catch (e) {
+ Logger.error(e);
+ return false;
+ }
+ }
+
+ _createZipFilename() {
+ let now = new Date(Date.now());
+ let year = now.getFullYear().toString();
+ let month = (now.getMonth()+1).toString();
+ let date = now.getDate().toString();
+ let hours = now.getHours().toString();
+ let minutes = now.getMinutes().toString();
+
+ // Pad single letter strings with preceding "0"
+ month = month.length < 2 ? "0" + month : month;
+ date = date.length < 2 ? "0" + date : date;
+ hours = hours.length < 2 ? "0" + hours : hours;
+ minutes = minutes.length < 2 ? "0" + minutes : minutes;
+
+ return `PairDrop_files_${year}${month}${date}_${hours}${minutes}.zip`;
+ }
+
+ async _setViaShareMenu(files) {
+ await this._setShareButton(files);
+
+ // always show dialog
+ this.show();
+ // open share menu automatically
+ setTimeout(() => {
+ this.$shareBtn.click();
+ }, 500);
+ }
+
+ async _setViaDownload(peerId, files, totalSize, descriptor) {
+ await this._setDownloadButton(peerId, files, totalSize, descriptor);
+
+ if (!this.sendAsZip && files.length !== 1) {
+ this.show();
+ return;
+ }
+
+ // download automatically if zipped or if only one file is received
+ this.$downloadBtn.click();
+ // if automatic download fails -> show dialog
+ setTimeout(() => {
+ if (!this.downloadSuccessful) {
+ this.show();
+ }
+ }, 1000);
+ }
+
+ _tidyUpButtons() {
+ this.$shareBtn.setAttribute('disabled', true);
+ this.$shareBtn.setAttribute('hidden', true);
+ this.$shareBtn.onclick = null;
+ this.$downloadBtn.setAttribute('disabled', true);
+ this.$downloadBtn.setAttribute('hidden', true);
+ this.$downloadBtn.onclick = null;
+ }
+
+ _tidyUpPreviewBox() {
+ this.$previewBox.innerHTML = '';
+ }
+
hide() {
super.hide();
setTimeout(async () => {
- this.$shareBtn.setAttribute('hidden', true);
- this.$downloadBtn.setAttribute('disabled', true);
- this.$previewBox.innerHTML = '';
+ this._tidyUpButtons();
+ this._tidyUpPreviewBox();
+
this._busy = false;
+
await this._nextFiles();
}, 300);
}
@@ -1213,29 +1341,31 @@ class ReceiveRequestDialog extends ReceiveDialog {
this._showRequestDialog(request, peerId)
}
+ _addThumbnailToPreviewBox(thumbnailData) {
+ if (thumbnailData && thumbnailData.substring(0, 22) === "data:image/jpeg;base64") {
+ let element = document.createElement('img');
+ element.src = thumbnailData;
+ this.$previewBox.appendChild(element)
+ }
+ }
+
_showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId;
- const displayName = $(peerId).ui._displayName();
- const connectionHash = $(peerId).ui._connectionHash;
-
- const badgeClassName = $(peerId).ui._badgeClassName();
-
- this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize, badgeClassName);
-
- if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") {
- let element = document.createElement('img');
- element.src = request.thumbnailDataUrl;
- this.$previewBox.appendChild(element)
- }
-
- const transferRequestTitle= request.imagesOnly
+ const transferRequestTitleTranslation = request.imagesOnly
? Localization.getTranslation('document-titles.image-transfer-requested')
: Localization.getTranslation('document-titles.file-transfer-requested');
- this.$receiveTitle.innerText = transferRequestTitle;
+ const displayName = $(peerId).ui._displayName();
+ const connectionHash = $(peerId).ui._connectionHash;
+ const badgeClassName = $(peerId).ui._badgeClassName();
- document.title = `${transferRequestTitle} - PairDrop`;
+ this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize, badgeClassName);
+ this._addThumbnailToPreviewBox(request.thumbnailDataUrl);
+
+ this.$receiveTitle.innerText = transferRequestTitleTranslation;
+
+ document.title = `${transferRequestTitleTranslation} - PairDrop`;
changeFavicon("images/favicon-96x96-notification.png");
this.$acceptRequestBtn.removeAttribute('disabled');
@@ -1249,6 +1379,7 @@ class ReceiveRequestDialog extends ReceiveDialog {
})
if (accepted) {
Events.fire('set-progress', {peerId: this.correspondingPeerId, progress: 0, status: 'wait'});
+ // Todo: only on big files?
NoSleepUI.enable();
}
this.hide();