From 3dd96b41eb9150b9b097ef3b4ff5f1bd6aec52a5 Mon Sep 17 00:00:00 2001 From: Sri Harsha D V Date: Sun, 18 May 2025 14:55:52 +0530 Subject: [PATCH] feat: add file name change functionality --- public/index.html | 22 ++++++ public/lang/en.json | 2 + public/scripts/ui.js | 136 +++++++++++++++++++++++++++++++++- public/styles/styles-main.css | 9 +++ 4 files changed, 166 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 70eda5b..3396563 100644 --- a/public/index.html +++ b/public/index.html @@ -501,6 +501,17 @@
+
+
+ +
+ + +
+
+ +
+
@@ -531,6 +542,17 @@
+
+
+ +
+ + +
+
+ +
+
diff --git a/public/lang/en.json b/public/lang/en.json index 54a05c6..885f2ef 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -70,6 +70,8 @@ "join": "Join", "leave": "Leave", "would-like-to-share": "would like to share", + "change-name": "Change name :", + "reset-name": "Reset Name", "accept": "Accept", "decline": "Decline", "has-sent": "has sent:", diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 42da3bc..47f3682 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -826,6 +826,10 @@ class LanguageSelectDialog extends Dialog { } class ReceiveDialog extends Dialog { + // These variables are made static so that they can be accessed and modified across child classes. + static fileNameInputValue = ""; + static fileNameInputExtValue = ""; + constructor(id) { super(id); this.$fileDescription = this.$el.querySelector('.file-description'); @@ -836,6 +840,13 @@ class ReceiveDialog extends Dialog { this.$fileSize = this.$el.querySelector('.file-size'); this.$previewBox = this.$el.querySelector('.file-preview'); this.$receiveTitle = this.$el.querySelector('h2:first-of-type'); + + // Store reserved file names initially in an array + this.reservedNames = ["con", "prn", "aux", "nul"]; + for (let i=1; i <= 9; i++) { + this.reservedNames.push(`com${i}`); + this.reservedNames.push(`lpt${i}`); + } } _formatFileSize(bytes) { @@ -855,6 +866,24 @@ class ReceiveDialog extends Dialog { } } + resetFileNameInput(files, fileNameInput, fileNameInputExt) { + // Reset file name inside input text box and file name extension. + const fileName = files[0].name; + const fileNameSplit = fileName.split('.'); + const fileExtension = fileNameSplit.length > 1 + ? '.' + fileNameSplit[fileNameSplit.length - 1] + : ''; + if (files.length === 1) { + fileNameInput.value = fileName.substring(0, fileName.length - fileExtension.length); + fileNameInputExt.innerText = fileExtension; + } else { + fileNameInput.value = "PairDrop_files_{YYYY}{MM}{DD}_{hh}{mm}"; + fileNameInputExt.innerText = ".zip"; + } + ReceiveDialog.fileNameInputValue = fileNameInput.value; + ReceiveDialog.fileNameInputExtValue = fileNameInputExt.innerText; + } + _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) { let fileOther = ""; @@ -882,7 +911,48 @@ class ReceiveDialog extends Dialog { this.$displayName.innerText = displayName; this.$displayName.title = connectionHash; this.$displayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id"); - this.$displayName.classList.add(badgeClassName) + this.$displayName.classList.add(badgeClassName); + } + + validateFileNameInput(fileNameInput, fileNameError, btn) { + // File name with invalid characters ("/", "\", ":", "*", "?", """, "<" and ">") or reserved names or empty string is considered invalid. + // If the file name is invalid, then respective error is displayed and the button to download is disabled. + const fileNameInputVal = fileNameInput.value; + if (fileNameInputVal.includes("/")) { + fileNameError.innerText = "File name should not contain '/'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes("\\")) { + fileNameError.innerText = "File name should not contain '\\'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes(":")) { + fileNameError.innerText = "File name should not contain ':'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes("*")) { + fileNameError.innerText = "File name should not contain '*'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes("?")) { + fileNameError.innerText = "File name should not contain '?'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes("\"")) { + fileNameError.innerText = "File name should not contain '\"'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes("<")) { + fileNameError.innerText = "File name should not contain '<'."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.includes(">")) { + fileNameError.innerText = "File name should not contain '>'."; + btn.setAttribute('disabled', true); + } else if (this.reservedNames.includes(fileNameInputVal.toLowerCase())) { + fileNameError.innerText = "Reserved names cannot be set as file names."; + btn.setAttribute('disabled', true); + } else if (fileNameInputVal.trim() === "") { + fileNameError.innerText = "File name cannot be empty."; + btn.setAttribute('disabled', true); + } else { + ReceiveDialog.fileNameInputValue = fileNameInputVal; + fileNameError.innerText = ""; + btn.removeAttribute('disabled'); + } } } @@ -893,6 +963,11 @@ class ReceiveFileDialog extends ReceiveDialog { this.$downloadBtn = this.$el.querySelector('#download-btn'); this.$shareBtn = this.$el.querySelector('#share-btn'); + this.$fileNameInput = this.$el.querySelector('#file-name-input-2'); + this.$fileNameInputExt = this.$el.querySelector('#file-name-input-ext-2'); + this.$resetNameBtn = this.$el.querySelector('#reset-name-2'); + this.$fileNameError = this.$el.querySelector('#file-name-error-2'); + this.$fileNameInput.addEventListener('input', _ => this.validateFileNameInput(this.$fileNameInput, this.$fileNameError, this.$downloadBtn)); Events.on('files-received', e => this._onFilesReceived(e.detail.peerId, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); this._filesQueue = []; @@ -960,9 +1035,42 @@ class ReceiveFileDialog extends ReceiveDialog { }); } + adjustFileName(fileName) { + // Method to replace the placeholders inside file name with respective timestamp values + let newFileName = fileName; + 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; + newFileName = newFileName.replaceAll("{YYYY}", year); + newFileName = newFileName.replaceAll("{MM}", month); + newFileName = newFileName.replaceAll("{DD}", date); + newFileName = newFileName.replaceAll("{hh}", hours); + newFileName = newFileName.replaceAll("{mm}", minutes); + console.log(newFileName); + return newFileName; + } + async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) { this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName); + // Adjust the input content initially + this.$fileNameInput.value = ReceiveDialog.fileNameInputValue; + this.$fileNameInputExt.innerText = ReceiveDialog.fileNameInputExtValue; + this.$fileNameError.innerText = ""; + + this.$resetNameBtn.addEventListener("click", _ => { + this.resetFileNameInput(files, this.$fileNameInput, this.$fileNameInputExt); + this.$fileNameError.innerText = ""; + this.$downloadBtn.removeAttribute('disabled'); + }); + let descriptor, url, filenameDownload; if (files.length === 1) { descriptor = imagesOnly @@ -1029,7 +1137,11 @@ class ReceiveFileDialog extends ReceiveDialog { this.$downloadBtn.onclick = _ => { if (downloadZipped) { let tmpZipBtn = document.createElement("a"); - tmpZipBtn.download = filenameDownload; + if (ReceiveDialog.fileNameInputValue !== "") { + tmpZipBtn.download = this.adjustFileName(ReceiveDialog.fileNameInputValue)+ReceiveDialog.fileNameInputExtValue; + } else { + tmpZipBtn.download = filenameDownload; + } tmpZipBtn.href = url; tmpZipBtn.click(); } @@ -1080,7 +1192,11 @@ class ReceiveFileDialog extends ReceiveDialog { _downloadFilesIndividually(files) { let tmpBtn = document.createElement("a"); for (let i=0; i this._respondToFileTransferRequest(true)); this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); + this.$fileNameInput = this.$el.querySelector('#file-name-input-1'); + this.$fileNameInputExt = this.$el.querySelector('#file-name-input-ext-1'); + this.$resetNameBtn = this.$el.querySelector('#reset-name-1'); + this.$fileNameError = this.$el.querySelector('#file-name-error-1'); + this.$fileNameInput.addEventListener('input', _ => this.validateFileNameInput(this.$fileNameInput, this.$fileNameError, this.$acceptRequestBtn)); Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId)) Events.on('keydown', e => this._onKeyDown(e)); @@ -1143,6 +1264,15 @@ class ReceiveRequestDialog extends ReceiveDialog { this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize, badgeClassName); + // Initially reset the file name inside input textbox + this.resetFileNameInput(request.header, this.$fileNameInput, this.$fileNameInputExt); + + this.$resetNameBtn.addEventListener("click", _ => { + this.resetFileNameInput(request.header, this.$fileNameInput, this.$fileNameInputExt); + this.$fileNameError.innerText = ""; + this.$acceptRequestBtn.removeAttribute('disabled'); + }); + if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); element.src = request.thumbnailDataUrl; diff --git a/public/styles/styles-main.css b/public/styles/styles-main.css index 32ffc46..f48300e 100644 --- a/public/styles/styles-main.css +++ b/public/styles/styles-main.css @@ -297,6 +297,10 @@ h3 { color: white !important; } +.text-red { + color: red !important; +} + .font-body1, body { font-size: 14px; @@ -678,6 +682,11 @@ button::-moz-focus-inner { border: 0; } +#reset-name-1, #reset-name-2 { + margin: 3px; + height: 50px; +} + /* Icon Button */ .icon-button {