JavaScript

Creating renderer.js file

In this tutorial we’ll create renderer.js file. This file is the part of index.html.The renderer.js file controls the whole application, it receives the inputs from the users, send commands to walkerHelper.js to start, pause or reset the walker.js, display the results on webview (grid.html) and update the app’s status bar (index.html).

The responsibilities of renderer.js file are:

  • Respond to users, such as, when a user click on a button or want to quit
  • Handle child processes
  • Communicate with walkerHelper.js
  • Send results to webview
  • Show dialog boxes
  • Close app
Duplicate File Finder GUI
Searching duplicate files from D:\personal folder

Let’s start writing the code:

const
remote = require('electron').remote,
cp     = require('child_process'),

The remote module helps to access modules restricted to main process, such as, dialog module for opening directories and displaying confirmation boxes. See Using Electron’s remote module.

The child_process module helps to execute walkerHelper.js as a child process. See Child Process in Node.

walker = cp.fork('./walkerkHelper.js'),
OPT    = require('./options.js'),

The cp.fork('./walkerkHelper.js') method returns a new communicable child process. See Create walkerHelper.js to control file walker.

The options.js returns an object storing app configuration. See Create app options module.

App UI

Duplicate File Finder app has five states: started, stopped, paused, resumed and done.

  1. Application close button, shows a confirmation dialog box to close the current window.
  2. Add folder button, shows directory selector dialog box. This button gets change when the state of app changes. The Add folder button get replace with following buttons when app change its state:
    • Add folder button shows when app state is stopped
    • Cancel button shows when state is started, paused or resumed. The Cancel button send the reset command to walkerHelper.js which removes the all pending entries and emit the done event.
    • New button shows when state is done. The New button reset the GUI to default and change state to stopped so the Add folder button appears.
  3. Start button starts the application for scanning the provided folders. This button also gets change when the state of app changes. The Start button replace with following four button:
    • Start button shows when the app state is stopped. This button is used to start the folders scanning. The stat of application will change to started when application successfully start scanning.
    • Pause button shows when state is started or resumed. This button is used to pause folder scanning.
    • Resume button shows when state is paused. This button is used to resume folder scanning.
    • Finished disabled button shows when scanning completed and state changed to done
  4. Heading div for added directories
  5. List of directories / folders to scan
  6. The WebView tag which loads the grid.html to display the duplicate results.
  7. App status bar shows Ready, Paused, Done or a folder path (which is currently being scanned).
  8. Delete location (path) from the list
  9. Location status. The location (path) will not scan if Excluded
  10. Directory location (path).
    Lets star writing the renderer.js file:
//renderer.js
// 1 The close button
appCloseBtn  = document.querySelector('#close'),

// 2 The Add folder button
addBtn       = document.querySelector('#addBtn'),

// 3 The Start button
startBtn     = document.querySelector('#startBtn'),

// 4 Heading div
pathsDivHead = document.querySelector('#pathsHead'),

// 5 Shows a list of directories added by Add folder button 
pathsDiv     = document.querySelector('#paths'),

// 6 WebView tag, loads grid.html
webview      = document.querySelector('webview'),

// 7 App's status bar
appStatusBar = document.querySelector('#status');

Next, write the following code:

let
debug = false,
dir   = dirObject,
dirs  = [],

Set debug to true if you want to see error messages on Chromium devtools. dir creates a reference to dirObject , see Making GUI - Creating dirObject class. dirs stores the dir objects.

prevStatusBarMsg = '',
appStatus        = OPT.STOPPED;

The prevStatusBarMsg stores the current working path when a user hit the Pause button and prints the stored path on the status bar when user hit the Resume button.

The appStatus stores the current app status, the default status is STOPPED.

A confirmation box will appear when a user clicks on close x button.
//Show confirmation box before closing the window
appCloseBtn.addEventListener('click', e =>{
 const options = {
  type: 'warning',
  buttons: ['Yes', 'No'],
  message: 'Do you really want to quit?'
 }
 remote.dialog.showMessageBox(options, i => {
  if (i == 1) return;
  remote.getCurrentWindow().close();
 })
});

A Yes No confirmation box will appear when a user hits the close button.

startBtn.addEventListener('click',()=>{
 debug&&console.log(appStatus);
 if (dirs.length === 0) return
 switch (appStatus){
  case OPT.STOPPED:
   startApp();
  break;
  case OPT.PAUSED:
   resumeApp();
  break;
  case OPT.STARTED:
  case OPT.RESUMED:
   pauseApp();
 }
});

As we already discussed , the startBtn is used to start, pause or resume the application.

addBtn.addEventListener('click', () =>{
 debug&&console.log(appStatus);
 switch (appStatus){ 
  case OPT.STOPPED:
   appStoppedAddBtnClicked();
  break;
  case OPT.DONE:
   appDoneAddBtnClicked();
  break;
  case OPT.PAUSED:
  case OPT.STARTED:
  case OPT.RESUMED:
   stopAppAddBtnClicked();
 }
});

If the app status is stopped the addBtn displays the directory selector dialog box by executing the appStoppedAddBtnClicked() method.

And, if the app status is done the addBtn displays the confirmation dialog box to reset the whole app (file walker, dir objects and clear the queue) by executing the appDoneAddBtnClicked() method.

If the app status is paused, started or resumed the addBtn display the confirmation box to cancel the current scanning by executing the stopAppAddBtnClicked() method.

The directory selector dialog box. Shows when click on Add folder button.
function appStoppedAddBtnClicked(){
 let options = {
  properties: ['openDirectory', 'multiSelections']
 } 
 remote.dialog.showOpenDialog(options, (paths) => {
  if (!paths) return
  paths.forEach(path =>{
   let isExist = dirs.some((dir) => {
    return dir.path == path
   })
   if (isExist){
    alert(path + ' already added in the list') ;
    return;
   }
   new dir(path);
  })
 })
}

The appStoppedAddBtnClicked() method shows the directory selector dialog box. The new dir object will create or display a message if the selected path already added.

Confirmation box to reset the whole app
function appDoneAddBtnClicked(){
 const options = {
  type: 'warning',
  buttons: ['Yes', 'No'],
  message: 'Do you wan to start a new empty project?'
 }
 remote.dialog.showMessageBox(options, i => {
  if (i == 1) return; 
  walker.send({walker:OPT.RESET});
  dirs = [];
  pathsDiv.innerHTML = '';
  pathsDivHead.className = 'hide';
  addBtn.innerHTML = '<span class="icon-folder"></span>Add folder';
  startBtn.innerHTML = '<span class="icon-play"></span>Start';
  startBtn.disabled = false;
  appStatusBar.innerHTML = 'Ready';
  webview.style.height = '';
  webview.reload();
  appStatus = OPT.STOPPED;
 })
}

The appDoneAddBtnClicked() method displays a confirmation dialog box when addBtn clicked. After successful confirmation this method resets the whole application.

The confirmation dialog box to cancel the existing scanning/ task
function stopAppAddBtnClicked(){
 const currAppStatus = appStatus;
 if (currAppStatus !== OPT.PAUSED){
  pauseApp();
 }
 const options = {
  type: 'warning',
  buttons: ['Yes', 'No'],
  message: 'Do you really want to cancel duplicate files search?'
 }
 remote.dialog.showMessageBox(options, i => {
  if (i == 1) {
   if (currAppStatus !== OPT.PAUSED){
    resumeApp();
   }
   return;
  } 
  resumeApp();
  walker.send({walker:OPT.RESET});
 })
}

The stopAppAddBtnClicked() method displays a confirmation box to stop the current task. It first stores the current status of app and then pause the application if it already not paused, then it shows the confirmation box. After successful confirmation it resume the app and send reset command to walker (child process of walkerHelper.js), which empties the queue and then walker returns the done event.

function startApp(){
 addBtn.disabled = true;
 startBtn.disabled = true;

 document
  .querySelectorAll('.delDir, .typeDir')
   .forEach(btn => {
     btn.className += ' disableBtn';
 });

 let obj = {
  walker:OPT.START,
  dirs:dirs
 }
 walker.send(obj);
}

The startApp() method disable all visible buttons, starts the scanning by sending the dirs array (storing dir objects added by Add folder button) and OPT.START command to walker (walkerHelper.js child process).

function resumeApp(){
 startBtn.disabled = true;
 let obj = {
  walker:OPT.RESUME
 }
 walker.send(obj);
}

function pauseApp(){
 startBtn.disabled = true;
 let obj = {
  walker:OPT.PAUSE
 }
 walker.send(obj);
}

The resumeApp() and pauseApp() methods are used to resume and pause the application.

Next, the walker.on receive the message from its child process (walkerHelper.js), for example, when “file walker” paused, stopped or done scanning :

walker.on('message', (m) => {
 switch (m.walker) {
  case OPT.WEBVIEW:
  webview.executeJavaScript(m.addRows, r => {
   r ? webview.style.height = r : '';
  });
 break;

 case OPT.DIR:
  appStatusBar.innerHTML = '&#128194; '+ m.dir;
 break;

 case OPT.PAUSED:
  appStatus = m.walker;
  startBtn.innerHTML = '<span class="icon-play"></span>Resume';
  startBtn.disabled  = false;
  prevStatusBarMsg   = appStatusBar.innerHTML;
  appStatusBar.innerHTML = 'Paused';
 break;

 case OPT.STARTED:
  addBtn.innerHTML = '<span class="icon-stop"></span>Cancel';
  addBtn.disabled  = false;
  startBtn.disabled= false;

 case OPT.RESUMED:
  appStatus = m.walker;
  startBtn.innerHTML = '<span class="icon-pause"></span>Pause';
  startBtn.disabled  = false;
  appStatusBar.innerHTML = prevStatusBarMsg;
 break;

 case OPT.DONE:
  appStatus = m.walker;
  startBtn.innerHTML = 'Finished';
  startBtn.disabled  = true;
  addBtn.innerHTML   = '<span class="icon-new"></span>New';
  appStatusBar.innerHTML = 'Done &nbsp; &nbsp; Scanned '+m.totalFiles+' files &amp; '+m.totalDirs+' folders';
  prevStatusBarMsg = '';
 break;
 }
});

To compare received message we’ll use switch case statement:

case OPT.WEBVIEW
walker child process sent the duplicate files. We’ll send these files to webview (grid.html).

case OPT.DIR
walker sent the current working directory. We’ll display it on the app’s status bar.

case OPT.PAUSED
walker paused the scanning. We’ll update the appStatus to paused, replace the icon and text of startBtn and display the Paused message on app’s status bar.

case OPT.STARTED
walker started the scanning. We’ll update the addBtn's icon and text.

case OPT.RESUMED
This code runs when walker started or resumed. We’ll update the startBtn icon and text and replace the status bar text with current working directory.

case OPT.DONE
walker finished the scanning of provided folders. We’ll update the startBtn text to Finished and make it disabled. We’ll also update the addBtn icon and text to display the New text. Lastly we’ll update the app’s status bar by writing the Done message, total scanned files and folders.

function dirObject (path){
 this.path       = path;
 this.isIncluded = true;
 dirs.push (this);
 
 pathsDivHead.className = '';
 
 // 8 The delete button
 let del = document.createElement('span');
 del.className = 'icon-close delDir';

 // 9 Include / Exclude button
 let type = document.createElement('span');
 type.className = 'typeDir';
 type.innerHTML = 'included';

 //10 Directory path
 let textNode = document.createTextNode(path),
 div = document.createElement('div');
 div.appendChild(del);
 div.appendChild(type);
 div.appendChild(textNode);
 pathsDiv.appendChild(div);

 del.addEventListener('click', () => {
  if (appStatus !== OPT.STOPPED) return;
  pathsDiv.removeChild(div);
  let index = dirs.indexOf(this);

  if (index !== -1)
   dirs.splice(index, 1);

  if (dirs.length === 0 )
   pathsDivHead.className = 'hide';
  })

 type.addEventListener('click', () => {
  if (appStatus !== OPT.STOPPED) return;
  this.isIncluded = !this.isIncluded;

  if (this.isIncluded){
   type.className = 'typeDir';
   type.innerHTML = 'included';
  }
  else {
   type.className = 'typeDir disableBtn';
   type.innerHTML = 'excluded';
  }
 })
}

The dirObject is responsible to create an object which holds the directory path and its status i.e. included or excluded. When a user clicks on Add folder button and select a folder, that folder then adds in the list as shown in above image at point # 5. See tutorial Creating the dirObject class for more information.

Click here to download the complete project (zip file).