Personal portfolio - Mauricio Aznar

Javascript electron

 

Introduction

Native node modules

Recommended to use electron-build (e.g. bycrypt)

Native node modules


Debugging

Debugging electron link

mainWindow.webContents.openDevTools()


App object

app

Prevent app from quitting

app.on('before-quit', (e) => {
  e.preventDefault()
})

Browser window

browser window

setup

use loadFile instead of loadURL

const { app, BrowserWindow } = require('content/snippets/javascript-electron')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')
}

app.on('ready', createWindow)

Load content

window.loadURL('https://myseite.com/index.html')
window.loadFile('index.html')
window.loadURL('file://Users/me/app/index.html')

Get user data (information of the user logged in)

app.getPath('userData')


Showing window gracefully

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true },
    show: false,
  })

  mainWindow.once('ready-to-show', mainWindow.show) 
}

Or...

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true },
    backgroundColor: '#2b2b3b'
  })
}

parent & child

parent & child

let mainW

function createWindow () {
  mainW = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  secondaryW = new BrowserWindow({
    width: 600, height: 300,
    webPreferences: { ndoeIntegration: true },
    parent: mainW,
    modal: true,
    show: false,
  })

  mainW.loadFile('index.html')
  secondaryW.loadFile('secondary.html')


  setTimeout(() => {
    secondaryW.show()
    setTimeout(() => {
      secondaryW.close()
      secondaryW = null
    }, 3000)
  }, 2000)

  mainW.on('closed', () => {
    mainW = null
  }) 
}

frameless window

frameless

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true },
    frame: false
  })

  mainWindow.loadFile('index.html') 
}

browser window properties and methods (options)

browser windows optios

State management on windows

electron-window-state package

npm install --save-dev electron-window-state

const windowStateKeeper = require('electron-window-state')

let mainWindow

function createWindow () {
  let winState = windowStateKeeper({
    defaultWidth: 1000,
    defaultHeight: 800
  })
  
  mainWindow = new BrowserWindow({
    width: winState.width,
    height: winState.height,
    x: winState.x,
    y: winState.y,
    webPreferences: { ndoeIntegration: true },
  })

  mainWindow.loadFile('index.html')
  
  winState.manage(mainWindow)
}

Web contents (broweser window)

Web contents

Instance vs static

const { app, BrowserWindow, webContents } = require('content/snippets/javascript-electron')

let mainWindow

function createWindow() {

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    x: 100,
    y: 100,
    webPreferences: { ndoeIntegration: true },
  })

  mainWindow.loadFile('index.html')

  // mainWindow.webContents.openDevTools()

  // web content instance
  let wc = mainWindow.webContents

  wc.on('did-finish-load', () => {
    console.log('All content (images or others content included)')
  })

  wc.on('dom-ready', () => {
    console.log('Dom loaded (all tags)')
  })

  // static method (from all web content instatnces)
  webContents.getAllWebContents()

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

Prevent window creation

wc.on('new-window', (e, url) => {
  e.preventDefault()
  console.log(`Preventing new window for: ${url}`)
})

Before event

wc.on('before-input-event', (e, input) => {
  e.preventDefault()
  console.log(`Preventing new window for: ${input}`)
})

Login

wc.on('login', (e, request, authInfo, callback) => {
  console.log('Logging in: ')
  callback('user', 'password')
})

wc.on('did-navigate', (e, url, statusCode, message) => {
  console.log(`Navigated to: ${url}`)
  console.log(statusCode)
})

Did navigate

wc.on('did-navigate', (e, url, statusCode, message) => {
  console.log(`Navigated to: ${url}`)
  console.log(statusCode)
})

Context menu

wc.on('context-menu', (e, params) => {
  console.log(`Context menu opened on: ${params.mediaType} at x:${params.x}, y:${params.y}`)
})

wc.on('context-menu', (e, params) => {
  console.log(`User selected text: ${params.selectionText}`)
  console.log(`Selection can be copied: ${params.editFlags.canCopy}`)
})


Execute javascript

wc.executeJavascript()


Session

Session

  • Session share sessions
let ses = mainWindow.webContents.session

Default session is also used

let defaultsSes = session.defaultSession

Custom session

const { session } = require('content/snippets/javascript-electron')

let customSession = session.fromPartition('part1')

let mainWindow
let secondWindow
let thirdWindow

const mainWindow = new BrowserWindow({
  width: 1000,
  height: 1000,
  x: 100,
  y: 100,
  webPreferences: {
    ndoeIntegration: true,
    session: customSession
  },
})


let persistedCustomSession = session.fromPartition('persist:part2')

const secondWindow = new BrowserWindow({
  width: 1000,
  height: 1000,
  x: 100,
  y: 100,
  webPreferences: {
    ndoeIntegration: true,
    session: persistedCustomSession
  },
})


// asiggned inside of browser window

const thirdWindow = new BrowserWindow({
  width: 1000,
  height: 1000,
  x: 100,
  y: 100,
  webPreferences: {
    ndoeIntegration: true,
    partition: 'persist:part3'
  },
})


clear session

clear on instance

ses.clearStorageData()


Cookies

Cookies

const { session } = require('content/snippets/javascript-electron')

let mainWindow

let customSession = session.fromPartition('part1')

function createWindow() {

  let ses = session.defaultSession

  // read cookies
  ses.cookies.get({})
    .then(cookies => {

    })
    .catch(e => {
    })

  let cookie = {
    url: "https://myappdomain.com",
    name: 'cookie1',
    value: 'electron',
    expirationDate: 1622818789
  }

  // set cookie
  ses.cookies.set(cookie)
    .then(() => {
      console.log('Cookie 1 set')
    })

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    x: 100,
    y: 100,
    webPreferences: { ndoeIntegration: true },
  })

  // get by name
  set.cookies.get({
    name: 'cookie1'
  })

  // remove cookie (url and name of cookie)
  ses.cookies.remove("https://myappdomain.com", 'cookie1')
    .then(() => {
    })


}



Download item

Download item

Use html attribute property download on a a tag

const { session } = require('content/snippets/javascript-electron')

let mainWindow

let customSession = session.fromPartition('part1')

function createWindow() {

  let ses = session.defaultSession

  // read cookies
  ses.cookies.get({})
    .then(cookies => {

    })
    .catch(e => {
    })

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    x: 100,
    y: 100,
    webPreferences: { ndoeIntegration: true },
  })

  // download item without any prompt (small file)
  ses.on(`will-download`, (e, downloadItem, webContents) => {
    console.log('starting download')
    let filename = downloadItem.getFilename()
    let filesize = downloadItem.getTotalBytes()

    // save to desktop
    downloadItem.setSavePath(app.getPath('desktop') + `/${filename}`)

    downloadItem.on('updated', (e, state) => {

      let received = downloadItem.getReceivedBytes()

      if (state === 'progressing' && received) {
        let progress = Math.Round((received / filesize) * 100)
        webContents.executeJavascript(`window.progress.value = ${progress}`)
      }

    })
  })


}



Dialog

Dialog

const { Dialog } = require('content/snippets/javascript-electron')

let mainWindow


function createWindow() {

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    x: 100,
    y: 100,
    webPreferences: { ndoeIntegration: true },
  })

  mainWindow.loadFile('index.html')


  // file explorer dialog

  mainWindow.webContents.on('did-finish-load', () => {

    Dialog.showOpenDialog(mainWindow, {
      buttonLabel: 'Select a photo',
      defaultPath: app.getPath('home'),
      properties: [
        'multiSelections',
        'createDirectory',
        'openFile',
        'openDirectory'
      ]
    })
      .then(result => {

      })


    Dialog.showSaveDialog({})
      .then(result => {

      })

    Dialog.showMessageBox({
      title: 'title',
      message: 'Please select an option',
      detail: 'Message details',
      buttons: ['yes', 'no', 'maybe']
    })
      .then(result => {
        console.log('button selected index ' + result.response)
      })
  })
}



accelerators & global shortcuts

const { globalShortcut } = require('content/snippets/javascript-electron')

globalShortcut.register('G', () => {
  console.log('user pressed g')
})

globalShortcut.register('CommandOrControl+G', () => {
  console.log('user pressed g with a combination key')
})

// once
globalShortcut.register('CommandOrControl+G', () => {
  console.log('user pressed g with a combination key')
  globalShortcut.unregister('CommandOrControl+G')
})

  • menu is like a system bar
  • could be exported to a separate file
  • roles (already integrated menu actions)
  • shortcuts are only triggered when app is in focus
const { app, BrowserWindow, Menu, MenuItem } = require('content/snippets/javascript-electron')

let mainWindow

let mainMenu = new Menu()

// in mac the first menu item is the name of the app
let menuItem1 = new MenuItem({
  label: 'Electron',
  submenu: [
    {
      label: 'Item 1',
      click: () => {
        console.log('item clicked')
      },
      accelerator: 'Shift + Alt + g'
    },
    {
      label: 'Item 2',
      submenu: {
        label: "Sub item 1"
      }
    },
    {
      role: 'toggleFullScreen'
    },
    {
      role: 'undo'
    },
    {
      role: 'redo'
    },
    {
      role: 'copy'
    },
    {
      role: 'paste'
    },
    {
      label: 'Item 3',
      enabled: false
    }
  ]
})

mainMenu.append(menuItem1)


let mainMenu2 = Menu.buildFromTemplate([
  {
    label: 'Electron',
    submenu: [
      { label: 'Item 1' },
      {
        label: 'Item 2',
        submenu: {
          label: "Sub item 1"
        }
      },
      { label: 'Item 3' }
    ]
  },
  {
    label: 'Action 2'
  },
  {
    label: 'Action 3'
  }
])


function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')

  Menu.setApplicationMenu(mainMenu)
}

app.on('ready', createWindow)


Context menu

const { app, BrowserWindow, Menu, MenuItem } = require('content/snippets/javascript-electron')

let mainWindow

let contextMenu = new Menu.buildFromTemplate([
  {
    label: 'item 1'
  },
  {
    label: 'item 2'
  }
])

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')

  mainWindow.webContents.on('context-menu', () => {
    contextMenu.popup()
  })
}

app.on('ready', createWindow)


Tray

Tray Native images

const { app, BrowserWindow, Menu, MenuItem, Tray } = require('content/snippets/javascript-electron')

let tray
let mainWindow


let trayMenu = Meny.buildFromTemplate([
  { label: 'Item 1' },
  { role: 'quit' }
])

function createTray() {
  tray = new Tray('trayTemplate@2x.png')
  tray.setTooltip('Tray details')

  tray.on('click', (e) => {

    if (e.shiftKey) {
      app.quit()
    } else {
      mainWindow.isVisible ? mainWindow.hide() : mainWindow.show()
    }
  })

  tray.setContextMeny(trayMenu)
}

let contextMenu = new Menu.buildFromTemplate([
  {
    label: 'item 1'
  },
  {
    label: 'item 2'
  }
])

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')

  mainWindow.webContents.on('context-menu', () => {
    contextMenu.popup()
  })
}

app.on('ready', createWindow)


Power Monitor

Power monitor

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow, Menu, MenuItem, Tray } = electron

let tray
let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000, height: 800,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')


  electron.powerMonitor.on('resume', (e) => {
    if (!mainWindow) {
      createWindow()
    }
  })

  electron.powerMonitor.on('suspend', (e) => {
    console.log('Saving some data')
  })
}

app.on('ready', createWindow)


Screen

  • used only when app is ready

Screen

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow, screen } = electron

let primaryDisplay = screen.getPrimaryDisplay()

let tray
let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: primaryDisplay.size.width / 2,
    height: primaryDisplay.size.height,
    x: primaryDisplay.bounds.x,
    y: primaryDisplay.bounds.y,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')

}

app.on('ready', createWindow)


Renderer process

Browser window proxy

Browser window proxy

  • we can control window properties
let win
const newWin = () => {
  win = window.open('https://developer.mozilla.org')  
}

const closeWin = () => {
  win.close()
}

<br />

Web frame

  • zoom
  • spelling and grammar
  • resources

Web frame

const electron = require('content/snippets/javascript-electron')
const { webFrame } = electron

webFrame.getZoomFactor() // 1 === 100%, 2 === 200%

Desktop capturer

const electron = require('content/snippets/javascript-electron')
const { desktopCapturer } = electron


desktopCapturer, getSources({
  type
})


IPC communication

IPC main IPC renderer

  • inter process communication

main.js

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow, ipcMain } = electron

let primaryDisplay = screen.getPrimaryDisplay()

let tray
let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: primaryDisplay.size.width / 2,
    height: primaryDisplay.size.height,
    x: primaryDisplay.bounds.x,
    y: primaryDisplay.bounds.y,
    webPreferences: { ndoeIntegration: true }
  })

  mainWindow.loadFile('index.html')


  mainWindow.webContents.on('did-finish-load', (e) => {
    mainWindow.webContents.send('mailbox', 'you have mail')
  })

}

ipcMain.on('channel1', (e, args) => {
  e.sender.send('channel1-response', 'Message received on channeld 1')
})

cMain.on('sync-message', (e, args) => {
  e.returnValue = 'return value'
})


app.on('ready', createWindow)


renderer.js

const electron = require('content/snippets/javascript-electron')
const { ipcRenderer } = electron

ipcRenderer.send('channel1', 'Message')

const response = ipcRenderer.sendSync('sync-message', 'Message')

ipcRenderer.on('channel1-response', (e, args) => {

})

ipcRenderer.on('mailbox', (e, args) => {
  console.log(args)
})

app.on('ready', createWindow)

Remote module

Remote

  • risky exposing node js to users
  • performance penalty
  • accessing electron node modules in renderer process

main.js

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow, ipcMain } = electron

let primaryDisplay = screen.getPrimaryDisplay()

let tray
let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: primaryDisplay.size.width / 2,
    height: primaryDisplay.size.height,
    x: primaryDisplay.bounds.x,
    y: primaryDisplay.bounds.y,
    webPreferences: {
      ndoeIntegration: true,
      enableRemoteModule: true
    }
  })

  mainWindow.loadFile('index.html')


  mainWindow.webContents.on('did-finish-load', (e) => {
    mainWindow.webContents.send('mailbox', 'you have mail')
  })

}


app.on('ready', createWindow)


renderer.js

const electron = require('content/snippets/javascript-electron')
const { remote } = electron
const { dialog, BrowserWindow } = remote

setTimeout(() => {
  dialog.showMessageBox({
    message: 'Dialog from renderer',
    buttons: ['One', 'Two']
  }).renderer(res => {
    console.log(res)
  })

  let win = new BrowserWindow({
    x: 50,
    y: 50,
    width: 300,
    height: 250
  })

  const app = remote.app

  let mainWindow = remote.getCurrentWindow()

  mainWindow.maximize()
}, 2000)


Disabled remote and calling electron modules from renderer

main.js

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow, ipcMain, dialog } = electron

let primaryDisplay = screen.getPrimaryDisplay()

let tray
let mainWindow


ipcMain.on('ask-fruit', (e) => {
  callDialog().then(answer => {
    e.reply('answer-fruit', answer)
  })
})

ipcMain.handle('ask-fruit2', (e) => {
  return callDialog()
})

async function callDialog() {
  const buttons = ['One', 'Two']

  const index = await dialog.showMessageBox({
    message: 'Dialog from renderer',
    buttons: buttons
  }).renderer(res => {
    console.log(res)
  })

  return buttons[index]
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: primaryDisplay.size.width / 2,
    height: primaryDisplay.size.height,
    x: primaryDisplay.bounds.x,
    y: primaryDisplay.bounds.y,
    webPreferences: {
      ndoeIntegration: true,
      enableRemoteModule: false
    }
  })

  mainWindow.loadFile('index.html')


  mainWindow.webContents.on('did-finish-load', (e) => {
    mainWindow.webContents.send('mailbox', 'you have mail')
  })

}


app.on('ready', createWindow)


renderer.js

const electron = require('content/snippets/javascript-electron')
const { ipcRenderer } = electro

ipcRenderer.send('ask-fruit')

ipcRenderer.on('answer-fruit', (e, arg) => {
  console.log(answer)
})

ipcRenderer.invoke('ask-fruit2').then(answer => {
  console.log(answer)
})



Shared api

Process

electron js process

  • available at main process
  • available at renderer process when node integration is true
  • available process methods
    • hang()
    • crash()

Shell

shell

use default applications for resource usage

  • openExternal
  • openPath
  • showItemInFolder
  • moveItemToTrash
const { shell } = require('content/snippets/javascript-electron')

Native images

native images

main.js

const { nativeImage, ipcMain } = require('content/snippets/javascript-electron')

let mainWindow


ipcMain.handle('app-path', () => {
  return app.getPath('desktop')
})


renderer.js

const { nativeImage, ipcRenderer } = require('content/snippets/javascript-electron')
const fs = require('fs')
const splash = nativeImage.createFromPath(`${__dirname}/splash.png`)
console.log(splash.getSize())
const toPng = e => {
  let pngSplash = splash.toPNG()
}
const toJpg = e => {
  let pngSplash = splash.toJPEG(100)
}
const toTag = e => {

  let size = splash.getSize()

  const resizedImage = splash.resize({ width: Math.round(size.width / 4), height: Math.round(size.height / 4) })

  let splashUrl = resizedImage.getDataUrl()
  document.getElementById('preview').src = splashUrl

}
const saveToDesktop = async (data, extension) => {
  let desktopPath = await ipcRenderer.invoke('app-path')
  fs.writeFile(desktopPath + '/' + extension, data, console.log)
}


Clipboard

Clipboard

const { clipboard } = require('content/snippets/javascript-electron')

console.log(clipboard.readText())

const contentOfClipboard = clipboard.readText()

clipboard.writeText('some text')

const image = clipboard.readImage()

image.toDataUrl()

Features & techniques

offscreen rendering

  • The "paint" event will fire each time a section of the webContents is rendered to a BrowserWindow. This happens regardless of the webContents being visible or not and is useful for handling off-screen content rendering.

offscreen rendering

main.js

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow } = electron
const fs = require('fs')


// dont use gpu, use cpu instead
app.disableHardwareAcceleration()

let primaryDisplay = screen.getPrimaryDisplay()

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: primaryDisplay.size.width / 2,
    height: primaryDisplay.size.height,
    x: primaryDisplay.bounds.x,
    y: primaryDisplay.bounds.y,
    show: false,
    webPreferences: {
      ndoeIntegration: true,
      offscreen: true
    }
  })

  mainWindow.loadUrl('https://electronjs.org')


  let i = 1
  mainWindow.webContents.on('paint', (e, dirty, image) => {
    let screenshot = image.toPNG()
    fs.writeFile(app.getPath('desktop') + `/screenshot_${i}.png`, screenshot, console.log)
    i++
  })


  mainWindow.webContents.on('did-finish-load', () => {
    console.log(mainWindow.getTitle())
    mainWindow.close()
    mainWindow = null
  })

}


app.on('ready', createWindow)


Network detection

Online & offline

  • exclusive to the renderer process
  • there are events that report if app is online or offline
const isOnline = navigator.onLine ? 'online' : 'offline'


Notifications

Notifications

  • clicking the notification focuses the app
const { remote } = require('content/snippets/javascript-electron')
const self = remote.getCurrentWindow()

let notification = new Notification('Electron app', {
  body: 'Some notification info'
})

notification.onclick = (e) => {
  if (!self.isVisible()) {
    self.show()
  }
}

Preload scripts

security

preload.js

const fs = require('fs') 

window.versions = {
  electron_v: process.versions.electron,
  writeContent: function (text) {
    fs.write(__dirname, '/a_file.txt', text, console.log)
  }
}

main.js

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow } = electron
const fs = require('fs')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    show: false,
    webPreferences: {
      ndoeIntegration: false,
      preload: __dirname + 'preload.js'
    }
  })

  mainWindow.loadUrl('https://electronjs.org')

}


app.on('ready', createWindow)

Progress bar

  • loading bar for an app depending on the operating system
  • mainWindow.setProgressBar(-1) remove the progress bar

Progress bar

const electron = require('content/snippets/javascript-electron')
const { app, BrowserWindow } = electron
const fs = require('fs')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 1000,
    show: false,
    webPreferences: {
      ndoeIntegration: false,
      preload: __dirname + 'preload.js'
    }
  })

  mainWindow.setProgressBar(0.25)

  mainWindow.loadUrl('https://electronjs.org')

}


app.on('ready', createWindow)

Application distribution

Electron builder

electron builder

CloudConvert is an online file converter. We support nearly all audio, video, document, ebook, archive, image, spreadsheet, and presentation formats. To get started, use the button below and select files to convert from your computer. Cloud convert


Code signing

Code Signing Certificates allow you to add digital signatures to your executables, enable software developers to include information about themselves and the integrity of their code with their software. The end users that download digitally signed 32-bit or 64-bit executable files (.exe, .ocx, .dll, .cab, and more) can be confident that the code really comes from you and has not been altered or corrupted since it was signed. Comodo ssl store Apple Developer


Publishing releases

Electron builder: A complete solution to package and build a ready for distribution Electron app for macOS, Windows and Linux with “auto update” support out of the box.

Auto update

Semantic versioning


AutoUpdater module

Auto update Electron log


MacOs

Apple id

Notarize

Electron notarize


Hardened runtime

The Hardened Runtime, along with System Integrity Protection (SIP), protects the runtime integrity of your software by preventing certain classes of exploits, like code injection, dynamically linked library (DLL) hijacking, and process memory space tampering. To enable the Hardened Runtime for your app, navigate in Xcode to your target’s Signing & Capabilities information and click the + button. In the window that appears, choose Hardened Runtime.

Hardened runtime


Touch bar

Apple touch bar Touch bar simulator