From 299e75eca328375468cc3d0bf34ae9ca73e445f6 Mon Sep 17 00:00:00 2001 From: Vikas Shashidhar Date: Tue, 27 Jun 2017 20:38:58 +0530 Subject: [PATCH] Electron 44 File Download Experience (#116) * Implemented File Download Experience 1. Initiate download manager when a file download is initiated. 2. Add items to download manager when new items are downloaded. 3. Allow user to open file in default OS app. 4. Allow user to show file in finder/explorer. 5. Allow user to close download bar. * 1. Removed underscore 2. Added safety checks 3. Added support for popouts 4. Creating most elements of download manager if electron * 1. Moved download manager logic to a separate file * 1. Added styles to help pushing up the content of the app when the download manager is being shown. * 1. Added tests for Download Manager. * Removed unnecessary code. * Made adjustments to handle positioning of the download manager through the flexbox model rather than the fixed positioning way. * Removed data attributes being added to body to handle download manager. --- js/downloadManager/downloadManager.js | 188 ++++++++++++++++++++++++++ js/main.js | 2 + js/preload/preloadMain.js | 2 + js/windowMgr.js | 20 +++ package.json | 4 +- tests/DownloadManager.test.js | 95 +++++++++++++ 6 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 js/downloadManager/downloadManager.js create mode 100644 tests/DownloadManager.test.js diff --git a/js/downloadManager/downloadManager.js b/js/downloadManager/downloadManager.js new file mode 100644 index 00000000..48ac2162 --- /dev/null +++ b/js/downloadManager/downloadManager.js @@ -0,0 +1,188 @@ +'use strict'; + +const { ipcRenderer, remote } = require('electron'); + +const local = { + ipcRenderer: ipcRenderer, + downloadItems: [] +}; + +// listen for file download complete event +local.ipcRenderer.on('downloadCompleted', (event, arg) => { + createDOM(arg); +}); + +// listen for file download progress event +local.ipcRenderer.on('downloadProgress', () => { + initiate(); +}); + +/** + * Open file in default app. + */ +function openFile(id) { + let fileIndex = local.downloadItems.findIndex((item) => { + return item._id === id + }); + if (fileIndex !== -1){ + let openResponse = remote.shell.openExternal(`file:///${local.downloadItems[fileIndex].savedPath}`); + if (!openResponse) { + remote.dialog.showErrorBox("File not found", 'The file you are trying to open cannot be found in the specified path.'); + } + } +} + +/** + * Show downloaded file in explorer or finder. + */ +function showInFinder(id) { + let showFileIndex = local.downloadItems.findIndex((item) => { + return item._id === id + }); + if (showFileIndex !== -1) { + let showResponse = remote.shell.showItemInFolder(local.downloadItems[showFileIndex].savedPath); + if (!showResponse) { + remote.dialog.showErrorBox("File not found", 'The file you are trying to access cannot be found in the specified path.'); + } + } +} + +function createDOM(arg) { + + if (arg && arg._id) { + + local.downloadItems.push(arg); + let downloadItemKey = arg._id; + + let ul = document.getElementById('download-main'); + if (ul) { + + let li = document.createElement('li'); + li.id = downloadItemKey; + li.classList.add('download-element'); + ul.insertBefore(li, ul.childNodes[0]); + + let itemDiv = document.createElement('div'); + itemDiv.classList.add('download-item'); + itemDiv.id = 'dl-item'; + li.appendChild(itemDiv); + let openMainFile = document.getElementById('dl-item'); + openMainFile.addEventListener('click', () => { + let id = openMainFile.parentNode.id; + openFile(id); + }); + + let fileDetails = document.createElement('div'); + fileDetails.classList.add('file'); + itemDiv.appendChild(fileDetails); + + let downProgress = document.createElement('div'); + downProgress.id = 'download-progress'; + downProgress.classList.add('download-complete'); + downProgress.classList.add('flash'); + setTimeout(() => { + downProgress.classList.remove('flash'); + }, 4000); + fileDetails.appendChild(downProgress); + + let fileIcon = document.createElement('span'); + fileIcon.classList.add('tempo-icon'); + fileIcon.classList.add('tempo-icon--download'); + fileIcon.classList.add('download-complete-color'); + setTimeout(() => { + fileIcon.classList.remove('download-complete-color'); + fileIcon.classList.remove('tempo-icon--download'); + fileIcon.classList.add('tempo-icon--document'); + }, 4000); + downProgress.appendChild(fileIcon); + + let fileNameDiv = document.createElement('div'); + fileNameDiv.classList.add('downloaded-filename'); + itemDiv.appendChild(fileNameDiv); + + let h2FileName = document.createElement('h2'); + h2FileName.classList.add('text-cutoff'); + h2FileName.innerHTML = arg.fileName; + fileNameDiv.appendChild(h2FileName); + + let fileProgressTitle = document.createElement('span'); + fileProgressTitle.id = 'per'; + fileProgressTitle.innerHTML = arg.total + ' Downloaded'; + fileNameDiv.appendChild(fileProgressTitle); + + let caret = document.createElement('div'); + caret.id = 'menu'; + caret.classList.add('caret'); + caret.classList.add('tempo-icon'); + caret.classList.add('tempo-icon--dropdown'); + li.appendChild(caret); + + let actionMenu = document.createElement('div'); + actionMenu.id = 'download-action-menu'; + actionMenu.classList.add('download-action-menu'); + caret.appendChild(actionMenu); + + let caretUL = document.createElement('ul'); + caretUL.id = downloadItemKey; + actionMenu.appendChild(caretUL); + + let caretLiOpen = document.createElement('li'); + caretLiOpen.id = 'download-open'; + caretLiOpen.innerHTML = 'Open'; + caretUL.appendChild(caretLiOpen); + let openFileDocument = document.getElementById('download-open'); + openFileDocument.addEventListener('click', () => { + let id = openFileDocument.parentNode.id; + openFile(id); + }); + + let caretLiShow = document.createElement('li'); + caretLiShow.id = 'download-show-in-folder'; + caretLiShow.innerHTML = 'Show in Folder'; + caretUL.appendChild(caretLiShow); + let showInFinderDocument = document.getElementById('download-show-in-folder'); + showInFinderDocument.addEventListener('click', () => { + let id = showInFinderDocument.parentNode.id; + showInFinder(id); + }); + } + } +} + +function initiate() { + let mainFooter = document.getElementById('footer'); + let mainDownloadDiv = document.getElementById('download-manager-footer'); + + if (mainDownloadDiv) { + + mainFooter.classList.remove('hidden'); + + let ulFind = document.getElementById('download-main'); + + if (!ulFind){ + let uList = document.createElement('ul'); + uList.id = 'download-main'; + mainDownloadDiv.appendChild(uList); + } + + let closeSpanFind = document.getElementById('close-download-bar'); + + if (!closeSpanFind){ + let closeSpan = document.createElement('span'); + closeSpan.id = 'close-download-bar'; + closeSpan.classList.add('close-download-bar'); + closeSpan.classList.add('tempo-icon'); + closeSpan.classList.add('tempo-icon--close'); + mainDownloadDiv.appendChild(closeSpan); + } + + let closeDownloadManager = document.getElementById('close-download-bar'); + if (closeDownloadManager) { + closeDownloadManager.addEventListener('click', () => { + local.downloadItems = []; + document.getElementById('footer').classList.add('hidden'); + document.getElementById('download-main').innerHTML = ''; + }); + } + } +} \ No newline at end of file diff --git a/js/main.js b/js/main.js index 1ec8f936..0d74cd86 100644 --- a/js/main.js +++ b/js/main.js @@ -11,6 +11,8 @@ const { isMac, isDevEnv } = require('./utils/misc.js'); const protocolHandler = require('./protocolHandler'); const getCmdLineArg = require('./utils/getCmdLineArg.js') +require('electron-dl')(); + // used to check if a url was opened when the app was already open let isAppAlreadyOpen = false; diff --git a/js/preload/preloadMain.js b/js/preload/preloadMain.js index 69bdbc53..96cdef62 100644 --- a/js/preload/preloadMain.js +++ b/js/preload/preloadMain.js @@ -19,6 +19,8 @@ const apiCmds = apiEnums.cmds; const apiName = apiEnums.apiName; const getMediaSources = require('../desktopCapturer/getSources'); +require('../downloadManager/downloadManager'); + const nodeURL = require('url'); // hold ref so doesn't get GC'ed diff --git a/js/windowMgr.js b/js/windowMgr.js index a84a7426..5c938cd2 100644 --- a/js/windowMgr.js +++ b/js/windowMgr.js @@ -6,6 +6,7 @@ const BrowserWindow = electron.BrowserWindow; const path = require('path'); const nodeURL = require('url'); const querystring = require('querystring'); +const filesize = require('filesize'); const { getTemplate, getMinimizeOnClose } = require('./menus/menuTemplate.js'); const loadErrors = require('./dialogs/showLoadError.js'); @@ -188,6 +189,25 @@ function doCreateMainWindow(initialUrl, initialBounds) { mainWindow.on('closed', destroyAllWindows); + // Manage File Downloads + mainWindow.webContents.session.on('will-download', (event, item, webContents) => { + // When download is in progress, send necessary data to indicate the same + webContents.send('downloadProgress'); + + // Send file path when download is complete + item.once('done', (event, state) => { + if (state === 'completed') { + let data = { + _id: getGuid(), + savedPath: item.getSavePath() ? item.getSavePath() : '', + total: filesize(item.getTotalBytes() ? item.getTotalBytes() : 0), + fileName: item.getFilename() ? item.getFilename() : 'No name' + }; + webContents.send('downloadCompleted', data); + } + }); + }); + // bug in electron is preventing this from working in sandboxed evt... // https://github.com/electron/electron/issues/8841 mainWindow.webContents.on('will-navigate', function(event, willNavUrl) { diff --git a/package.json b/package.json index 90fd8eca..1bc597f4 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,9 @@ "electron-context-menu": "^0.8.0", "electron-squirrel-startup": "^1.0.0", "keymirror": "0.1.1", - "winreg": "^1.2.3" + "winreg": "^1.2.3", + "electron-dl": "^1.9.0", + "filesize": "^3.5.10" }, "optionalDependencies": { "screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet.git#v1.0.1" diff --git a/tests/DownloadManager.test.js b/tests/DownloadManager.test.js new file mode 100644 index 00000000..c8120193 --- /dev/null +++ b/tests/DownloadManager.test.js @@ -0,0 +1,95 @@ +const downloadManager = require('../js/downloadManager/downloadManager'); +const electron = require('./__mocks__/electron'); +const $ = require('jquery'); + +describe('download manager', function() { + + describe('Download Manager to create DOM once download is initiated', function () { + + beforeEach(function () { + global.document.body.innerHTML = + '
' + + '
'; + }); + + it('should inject download bar element into DOM once download is initiated', function() { + + electron.ipcRenderer.send('downloadCompleted', {_id: '12345', fileName: 'test', total: 100}); + + expect(document.getElementsByClassName('text-cutoff')[0].innerHTML).toBe('test'); + expect(document.getElementById('per').innerHTML).toBe('100 Downloaded'); + + }); + + it('should inject multiple download items during multiple downloads', function() { + + electron.ipcRenderer.send('downloadCompleted', {_id: '12345', fileName: 'test', total: 100}); + electron.ipcRenderer.send('downloadCompleted', {_id: '67890', fileName: 'test1', total: 200}); + + let fileNames = document.getElementsByClassName('text-cutoff'); + + expect(fileNames[0].innerHTML).toBe('test1'); + expect(fileNames[1].innerHTML).toBe('test'); + expect(document.getElementById('per').innerHTML).toBe('100 Downloaded'); + + let downloadElements = document.getElementsByClassName('download-element'); + + expect(downloadElements[0].id).toBe('67890'); + expect(downloadElements[1].id).toBe('12345'); + + }); + + }); + + describe('Download Manager to initiate footer', function () { + + beforeEach(function () { + global.document.body.innerHTML = + ''; + }); + + it('should inject dom element once download is completed', function() { + + electron.ipcRenderer.send('downloadProgress'); + + expect(document.getElementById('download-manager-footer').classList).not.toContain('hidden'); + + }); + + it('should remove the download bar and clear up the download items', function() { + + electron.ipcRenderer.send('downloadProgress'); + + console.log(document.getElementById('download-manager-footer').classList); + expect(document.getElementById('download-manager-footer').classList).not.toContain('hidden'); + + $('#close-download-bar').click(); + expect(document.getElementById('download-manager-footer').classList).toContain('hidden'); + + }); + + }); + + describe('Download Manager to initiate footer', function () { + + beforeEach(function () { + global.document.body.innerHTML = + ''; + }); + + it('should inject ul element if not found', function() { + + electron.ipcRenderer.send('downloadProgress'); + + expect(document.getElementById('download-main')).not.toBeNull(); + expect(document.getElementById('download-manager-footer').classList).not.toContain('hidden'); + + }); + + }); + +}); \ No newline at end of file