cozy-labs/cozy-desktop

View on GitHub
gui/js/window_manager.js

Summary

Maintainability
C
7 hrs
Test Coverage
/** Base implementation for all windows.
 *
 * @module gui/js/window_manager
 */

const { BrowserWindow, ipcMain, shell } = require('electron')
const _ = require('lodash')
const path = require('path')
const capabilities = require('../../core/utils/capabilities')
const flags = require('../../core/utils/flags')

const ELMSTARTUP = 400

/*::
export type WindowBanner = {
  level: string,
  title: string,
  details: string
}
*/

const log = require('../../core/app').logger({
  component: 'windows'
})

module.exports = class WindowManager {
  constructor(app, desktop) {
    this.win = null
    this.app = app
    this.desktop = desktop
    this.log = require('../../core/app').logger({
      component: 'GUI/' + this.windowOptions().title
    })

    let handlers = this.ipcEvents()
    Object.keys(handlers).forEach(name => {
      if (!handlers[name]) {
        throw new Error('undefined handler for event ' + name)
      }
      ipcMain.on(name, handlers[name].bind(this))
    })
    ipcMain.on('renderer-error', (event, err) => {
      // Sender can be a WebContents instance not yet attached to this.win, so
      // we compare the title from browserWindowOptions:
      if (
        _.get(event, 'sender.browserWindowOptions.title') ===
        this.windowOptions().title
      ) {
        this.log.error({ err, sentry: true }, err.message)
      }
    })
  }

  /* abtract */
  windowOptions() {
    throw new Error('extend WindowManager before using')
  }

  /* abtract */
  ipcEvents() {
    throw new Error('extend WindowManager before using')
  }

  makesAppVisible() {
    return true
  }

  async show() {
    if (!this.win) await this.create()
    this.log.debug('show')

    // devTools
    if (process.env.WATCH === 'true' || process.env.DEBUG === 'true') {
      this.showDevTools()
    }

    this.win.show()

    return Promise.resolve(this.win)
  }

  showDevTools() {
    if (!this.win || this.win.webContents.isDestroyed()) return

    if (!this.devtools) {
      this.devtools = new BrowserWindow({ show: false })
      this.devtools.on('closed', () => {
        this.win.webContents.closeDevTools()
        this.devtools = null
      })
      this.win.webContents.setDevToolsWebContents(this.devtools.webContents)
    }

    this.win.on('hide', () => this.hideDevTools())
    this.win.on('closed', () => this.hideDevTools())

    this.win.webContents.openDevTools({ mode: 'detach' })
    this.devtools.show()
  }

  hideDevTools() {
    if (this.devtools) {
      this.devtools.hide()

      if (this.win) this.win.webContents.closeDevTools()
    }
  }

  hide() {
    if (this.win) {
      this.log.debug('hide')
      this.win.close()
    }
    this.win = null
  }

  shown() {
    return this.win != null
  }

  focus() {
    return this.win && this.win.focus()
  }

  reload() {
    if (this.win) {
      this.log.debug('reload')
      this.win.reload()
    }
  }

  send(...args) {
    this.win && this.win.webContents && this.win.webContents.send(...args)
  }

  on(event, handler) {
    this.win && this.win.on(event, handler)
  }

  once(event, handler) {
    this.win && this.win.once(event, handler)
  }

  async sendSyncConfig() {
    const { cozyUrl, deviceName, deviceId } = this.desktop.config
    this.send(
      'sync-config',
      cozyUrl,
      deviceName,
      deviceId,
      await capabilities(this.desktop.config),
      await flags(this.desktop.config)
    )
  }

  hash() {
    return ''
  }

  centerOnScreen(wantedWidth, wantedHeight) {
    try {
      this.win.setSize(wantedWidth, wantedHeight, true)
      this.win.center()
    } catch (err) {
      log.warn({ err, wantedWidth, wantedHeight }, 'Failed to centerOnScreen')
    }
  }

  create() {
    this.log.debug('create')
    const opts = {
      indexPath: path.resolve(__dirname, '..', 'index.html'),
      ...this.windowOptions()
    }
    opts.webPreferences = {
      ...opts.webPreferences,
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true
    }
    // https://github.com/AppImage/AppImageKit/wiki/Bundling-Electron-apps
    if (process.platform === 'linux') {
      opts.icon = path.join(__dirname, '../images/icon.png')
    }
    this.win = new BrowserWindow({
      autoHideMenuBar: true,
      show: false,
      ...opts
    })
    this.win.on('unresponsive', () => {
      this.log.warn('Web page becomes unresponsive')
    })
    this.win.on('responsive', () => {
      this.log.warn('Web page becomes responsive again')
    })
    this.win.webContents.on(
      'did-fail-load',
      (event, errorCode, errorDescription, url, isMainFrame) => {
        const err = new Error(errorDescription)
        err.code = errorCode
        this.log.error(
          { err, url, isMainFrame, sentry: true },
          'failed loading window content'
        )
      }
    )
    this.centerOnScreen(opts.width, opts.height)

    // openExternalLinks
    this.win.webContents.on('will-navigate', (event, url) => {
      if (
        url.startsWith('http') &&
        !url.match('/auth/authorize') &&
        !url.match('/auth/twofactor')
      ) {
        event.preventDefault()
        shell.openExternal(url)
      }
    })

    // noMenu
    this.win.setMenu(null)
    this.win.setAutoHideMenuBar(true)

    // Most windows (e.g. onboarding, help...) make the app visible in macOS
    // dock (and cmd+tab) by default. App is hidden when windows is closed to
    // allow per-window visibility.
    if (process.platform === 'darwin' && this.makesAppVisible()) {
      this.app.dock.show()
      const showTime = Date.now()
      this.win.on('closed', () => {
        const hideTime = Date.now()
        setTimeout(() => {
          this.app.dock.hide()
        }, 1000 - (hideTime - showTime))
      })
    }

    // dont keep  hidden windows objects
    this.win.on('closed', () => {
      this.win = null
    })

    const windowCreated = new Promise(resolve => {
      if (opts.show === false) {
        resolve(this.win)
      } else {
        this.win.webContents.on('dom-ready', () => {
          setTimeout(() => {
            this.win.show()
            resolve(this.win)
          }, ELMSTARTUP)
        })
      }
    }).catch(err => log.error({ err, sentry: true }, 'failed showing window'))

    this.win.loadURL(`file://${opts.indexPath}${this.hash()}`)

    return windowCreated
  }
}