skypager-project-types-electron-app
Version:
skypager electron app project type
824 lines (669 loc) • 21.3 kB
JavaScript
/**
* @name Panel
* @platform electron
* @description The Panel Helper is used to render the contents of Electron browser windows.
*/
import { createElement } from 'react'
import Helper from 'skypager-helper'
import { join } from 'path'
import url from 'url'
import isEmpty from 'lodash/isEmpty'
import isString from 'lodash/isString'
import mapValues from 'lodash/mapValues'
import isFunction from 'lodash/isFunction'
import partialRight from 'lodash/partialRight'
import omit from 'lodash/omit'
import defaults from 'lodash/defaultsDeep'
import Positioner from '../runtime/positioner'
import IPCResponder from '../runtime/responder'
import { ipcMain } from 'electron'
const { assign } = Object
export class Panel extends Helper {
static isCacheable = true
static attach(project) {
return Helper.attach(project, Panel, {
registryProp: 'panels',
lookupProp: 'panel',
registry: Helper.createContextRegistry('panels', {
context: require.context('./panels', true, /\.js$/),
dirname: __dirname,
filename: __filename,
})
})
}
initialize() {
this.applyMixin(this.get('provider.api', {}), this.context, this)
this.applyMixin(this.get('options.api', {}), this.context, this)
this.lazy('browserWindow', () => this.createBrowserWindow())
}
statusSnapshot() {
return {
url: this.url,
id: this.id,
name: this.name,
cacheKey: this.cacheKey,
optionKeys: Object.keys(this.options),
contextKeys: Object.keys(this.context),
layoutOptions: this.layoutOptions,
windowId: this.windowId,
paths: {
renderer: this.rendererRoot,
runtime: this.runtimeRoot,
main: this.mainRoot,
app: this.appPath,
},
windowDetails: {
id: this.windowId,
windowOptions: this.windowOptions,
bounds: this.windowBounds,
}
}
}
get ipcHandlers() {
const panel = this
const handlers = {
...this.get('appInstance.ipcHandlers', {}),
...this.tryGet('ipcHandlers', {})
}
return mapValues(handlers, (handler, id) => {
if (isFunction(handler)) {
return partialRight(handler.bind(panel), this.context)
} else if (isString(handler) && this.tryGet(handler)) {
return partialRight(this.tryGet(handler).bind(panel), this.context)
} else if (isString(handler) && this.get(`appInstance.${handler}`)) {
return this.get(`appInstance.${handler}`)
}
})
}
get appInstance() {
return this.get('context.appInstance', this.get('project.appInstance'))
}
createBrowserWindow() {
const panel = this
const win = new this.BrowserWindow( this.windowOptions )
Object.assign(win, {
windowId: win.id,
panelId: this.id,
panelName: this.name,
panelProject: this.get('project.cwd'),
getPanel() { return panel },
panelCacheKey: this.cacheKey,
positioner: new Positioner(win),
responder: new IPCResponder(win.webContents.send.bind(win.webContents), ipcMain.on.bind(ipcMain))
})
this.attachIPCHandlers(this.ipcHandlers, win)
this.setupEventBindings(win)
this.windowId = win.id
return win
}
async open(options = {}) {
const { files } = await this.appInstance.open(this.browserWindow, options)
return files
}
async save(options = {}) {
const { file } = await this.appInstance.save(this.browserWindow, options)
return file
}
get responder() {
return this.browserWindow.responder
}
get positioner() {
return this.browserWindow.positioner
}
createTray(...args) {
return this.appInstance.createTray(...args)
}
get hasBrowserWindow() {
return this.has('windowId')
}
async receiveMessage({channel, payload} = {}) {
const response = await this.appInstance.receivePanelMessage({channel, payload, panel: this})
return response
}
attachIPCHandlers(handlers, win = this.browserWindow) {
const panel = this
mapValues(handlers, (handler, channel) => {
win.responder.registerTopic(channel, (payload = {}) => {
return this.receiveMessage({channel, payload}).then((r) => handler(r))
})
})
return win
}
async tellPanel(payload = {}, defaultChannel = 'PANEL') {
const channel = payload.channel || defaultChannel
const response = await this.responder.tell(channel, omit(payload, 'channel'))
return response
}
async askPanel(payload, defaultChannel = 'PANEL') {
const channel = payload.channel || defaultChannel
const response = await this.responder.ask(channel, omit(payload, 'channel'))
return response
}
setupEventBindings(win) {
if (this.hasBrowserWindow) {
return win
}
if (this.tryGet('setupWebContents')) {
this.callMethod('setupWebContents', this.webContents)
}
win.on('unresponsive', () => {
this.isUnresponsive = true
setTimeout(() => {
if (this.isUnresponsive) {
if (this.tryGet('panelIsUnresponsive')) { this.callmethod('panelIsUnresponsive', win) }
this.handleUnresponsivePanel()
}
}, 3000)
})
win.on('unresponsive', () => {
if (this.tryGet('panelIsResponsive')) {
delete(this.isUnresponsive)
}
})
if (this.tryGet('windowDidMove')) {
win.on('move', (...args) => this.callMethod('windowDidMove', ({args, panel: this, win})))
}
if (this.tryGet('windowDidResize')) {
win.on('resize', (...args) => this.callMethod('windowDidResize', ({args, panel: this, win})))
}
if (this.tryGet('windowWillClose')) {
win.on('close', (...args) => this.callMethod('windowWillClose', ({args, win, panel: this})))
}
if (this.tryGet('windowDidClose')) {
win.once('closed', (...args) => {
this.callMethod('windowDidClose', ({args, win, panel: this}))
this.browserWindowClosed()
})
}
if (this.tryGet('windowDidHide')) {
win.on('hide', (...args) => this.callMethod('windowDidHide', ({ args, win, panel: this, })))
}
if (this.tryGet('windowDidRestore')) {
win.on('restore', (...args) => this.callMethod('windowDidRestore', ({
args,
win,
panel: this,
})))
}
if (this.tryGet('windowDidMaximize')) {
win.on('maximize', (...args) => this.callMethod('windowDidMaximize', ({
args,
win,
panel: this,
})))
}
if (this.tryGet('windowDidMinimize')) {
win.on('minimize', (...args) => this.callMethod('windowDidMinimize', ({
args,
win,
panel: this,
})))
}
if (this.tryGet('willNavigate')) {
win.webContents.on('will-navigate', function(event, url) {
this.callMethod('willNavigate', { panel: this, win, args: [event, url], event, url})
})
}
if (this.tryGet('didGetRedirectRequest')) {
win.webContents.on('did-get-redirect-request', function(event, oldUrl, newUrl) {
this.callMethod('didGetRedirectRequest', { panel: this, win, args: [event, oldUrl, newUrl], event, oldUrl, newUrl })
})
}
}
runSync(method, ...args) {
const handler = this.tryGet(method)
try {
return handler && handler.call(this, ...args)
} catch(error) {
console.log('Error will runningSync method', method, ...args)
console.log(error)
throw(error)
}
}
run(method, ...args) {
return Promise.resolve(this.runSync(method, ...args))
}
handleUnresponsivePanel() {
if (this.browserWindow) {
this.browserWindow.hide()
this.browserWindow.destroy()
}
}
async show(options = {}) {
if (this.tryGet('panelWillShow')) {
this.callMethod('panelWillShow', { panel: this, browserWindow: this.browserWindow, options, })
}
try {
this.project.debug('Debugging show method')
this.project.debug('Preparing')
const prepared = await this.prepare(options)
this.project.debug('Prepared')
const launched = await this.launch(options)
this.project.debug('launched')
if (this.tryGet('panelDidLaunch')) {
this.callMethod('panelDidLaunch', { panel: this, browserWindow: this.browserWindow })
}
} catch(error) {
if (this.tryGet('panelShowDidFail')) {
this.callMethod('panelShowDidFail', {
panel: this,
error,
})
}
}
if (this.tryGet('panelDidShow')) {
this.callMethod('panelDidShow', {
panel: this,
browserWindow: this.browserWindow,
bounds: this.browserWindow.getBounds(),
})
}
if (this.shouldShowDevTools) {
this.webContents.openDevTools()
}
return this
}
get shouldPreparePanel() {
return this.tryGet('preparePanels', this.get('appInstance.defaultPanelOptions.prepare', !this.isRemote))
}
get shouldGenerateContent() {
return this.tryGet('preparePanels', this.get('appInstance.defaultPanelOptions.generateContent', !this.isRemote))
}
async prepare(options = {}) {
if (!this.shouldPreparePanel) {
return this
}
if (this.isReady && !options.overwrite) {
return this
}
try {
await this.saveToDisk({
location: options.path || options.location || this.absolutePath,
layoutOptions: options,
})
this.isReady = true
} catch(error) {
this.isReady = true
this.prepareError = error
}
return this
}
async saveToDisk(options = {}) {
const {
location = this.absolutePath,
layoutOptions = {},
} = options
try {
await this.project.writeFileAsync(
location,
this.applyLayout(layoutOptions),
'utf8'
)
} catch(error) {
throw new Error(`Error saving the panel ${this.name} to disk at: ${this.absolutePath}: ${error.message}`)
}
return this
}
get url() {
return this.windowId ? urlWithArgs(this.webContents.getURL().replace(/\#.*$/, ''), this.args) : this.initialURL
}
get initialURL() {
return this.isRemote && !this.shouldPreparePanel
? urlWithArgs(this.remoteUrl, this.args)
: urlWithArgs(this.absolutePath, this.args)
}
get shouldShowDevTools() {
return this.tryGet('showDevTools', this.get('project.argv.showDevTools', false))
}
get isRemote () {
return typeof this.remoteHost !== 'undefined'
}
get remoteHost () {
return this.get('options.remoteHost', this.get('context.remoteHost'))
}
get remoteUrl() {
return this.result('options.url',
this.result('provider.url', `${this.remoteHost}/${this.filename}`)
)
}
get absolutePath() {
return this.project.resolve(join(this.rendererRoot, this.filename))
}
get BrowserWindow() {
return require('electron').BrowserWindow
}
get Renderer() {
return this.tryGet('Renderer') || this.tryGet('renderer')
}
get layout() {
const val = this.tryGet('layout')
return val === false
? (options) => options.content
: require('./templates/standard')
}
get layoutOptions() {
return this.pick(
'stylesheets',
'publicPath',
'headScripts',
'dllScripts',
'chunks',
'bodyId',
'bodyClass',
'htmlClass',
'initialState',
'headTop',
'headBottom',
'bodyTop',
'bodyBottom',
)
}
applyLayout (options = {}) {
let {
content = this.generateContent(omit(options, 'layout', 'layoutOptions')),
layoutOptions = {},
layout = this.layout
} = options
layoutOptions = {
...this.layoutOptions,
...layoutOptions,
content,
}
return layout.call(this, layoutOptions)
}
generateContent(options = {}) {
return this.Renderer && this.shouldGenerateContent
? this.invoke(this.rendererMethod, options)
: ''
}
get rendererMethod() {
return this.tryGet('rendererMethod', 'renderString')
}
renderMarkup(...args) {
return this.project.renderMarkup(this.Renderer, ...args)
}
renderString(...args) {
return this.project.renderString(this.Renderer, ...args)
}
createElement(...args) {
return createElement(this.Renderer, ...args)
}
launch(options = {}) {
let shouldTimeout = options.timeout !== false && options.timeout !== 0
let resolved = false
if (this.isLaunched && !options.overwrite) {
return Promise.resolve(this)
}
const { timeout = 8000, url = this.initialURL, showImmediately = options.show || this.showImmediately } = options
this.project.debug('Launching', { timeout, url })
return new Promise((resolve,reject) => {
try {
const browserWindow = this.browserWindow
this.project.debug(`BrowserWindow object ${ timeout } ${ browserWindow.id }`, { options })
const _timeout = this._timeout = this._timeout || setTimeout(() => {
this.project.debug(`Timeout Hit ${timeout} `, {
id: browserWindow.id,
name: this.name,
timeout,
shouldTimeout,
isLaunched: this.isLaunched,
})
if (shouldTimeout && !this.isLaunched && !browserWindow.isVisible()) {
throw new Error('Timeout while waiting for window')
} else if (shouldTimeout && browserWindow.isVisible()) {
clearTimeout(_timeout)
delete(this._timeout)
this.project.debug('Timeout hit. Launching already visible')
resolve(browserWindow)
} else if (resolved) {
delete(this._timeout)
clearTimeout(_timeout)
this.project.debug('Resolved. Launching already visible')
resolve(browserWindow)
}
}, timeout)
if (showImmediately) {
this.project.debug('Showing Immediately')
browserWindow.loadURL(url)
browserWindow.show()
shouldTimeout = false
clearTimeout(_timeout)
delete(this._timeout)
resolved = true
resolve(browserWindow)
} else {
browserWindow.once('ready-to-show', () => {
browserWindow.show()
shouldTimeout = false
this.project.debug('Ready to Show')
clearTimeout(_timeout)
delete(this._timeout)
})
this.project.debug(`Loading URL ${url}`)
browserWindow.loadURL(url)
resolve(browserWindow)
}
} catch(error) {
shouldTimeout = false
clearTimeout(_timeout)
reject(error)
}
}).then(() => {
this.project.debug('Is Launched. Clearing Timeout')
this.isLaunched = true
//clearTimeout(this._timeout)
return this
}).catch((error) => {
this.project.error(`Eror launching panel ${this.name}`, { error })
shouldTimeout = false
clearTimeout(this._timeout)
delete(this._timeout)
this.launchError = error
resolved = false
})
}
hide() {
this.browserWindow.hide()
return this
}
loadURL(url) {
this.browserWindow.loadURL(url || this.initialURL)
return this
}
get appPanelOptions() {
return this.get('appInstance.defaultPanelOptions', {})
}
get appOptions() {
return this.get('appInstance.panelOptions', this.get('appInstance.defaultPanelOptions', {}))
}
fetchValue(key, defaultValue) {
return this.tryGet(key) || this.get(['appOptions', key], defaultValue)
}
fetchResult(key, defaultValue) {
return this.tryResult(key) || this.result(['appOptions', key], defaultValue)
}
get bodyClass() {
return this.fetchResult('bodyClass')
}
get bodyId() {
return this.fetchResult('bodyId')
}
get htmlClass() {
return this.fetchResult('htmlClass')
}
get stylesheets() {
return this.fetchResult('stylesheets', [])
}
get chunks() {
return this.fetchResult('chunks', [])
}
get dllScripts() {
return this.fetchResult('dllScripts', [])
}
get headScripts() {
return this.fetchResult('headScripts', [])
}
get headTop() {
return this.fetchResult('headTop', '')
}
get headBottom() {
return this.fetchResult('headBottom', '')
}
get bodyTop() {
return this.fetchResult('bodyTop', '')
}
get bodyBottom() {
return this.fetchResult('bodyBottom', '')
}
get initialState() {
return this.fetchResult('initialState', this.fetchResult('injectState'))
}
get publicPath() {
return this.fetchResult('publicPath', this.isRemote ? this.remoteHost : '')
}
get showImmediately() {
return !!this.tryResult('showImmediately', this.get('windowOptions.show', false))
}
get webPreferences() {
return defaults({}, this.result('options.webPreferences'), this.result('provider.webPreferences'), this.result('appOptions.webPreferences', {
preload: this.preload,
devTools: this.shouldShowDevTools,
}))
}
get windowOptions() {
return defaults({}, this.result('options.window'), this.result('provider.window'), {
show: false,
preload: this.preload,
webPreferences: this.webPreferences
})
}
get preload() {
const setting = this.tryResult('preload')
return setting === false
? false
: setting || join(this.runtimeRoot, 'preload.js')
}
get filename() {
return this.fetchResult('filename', `${this.name}.html`)
}
get args() {
return {
windowId: this.windowId, ...this.fetchResult('args', {}),
}
}
get appPath() { return this.project.resolve(this._appPath) }
get rendererRoot() { return this.project.resolve(this._rendererRoot) }
get runtimeRoot() { return this.project.resolve(this._runtimeRoot) }
get mainRoot() { return this.project.resolve(this._mainRoot) }
get _appPath() {
return this.get('options.appPath', this.get('context.appPath', require('electron').app.getAppPath()))
}
get _rendererRoot() {
return this.get('options.rendererRoot', this.get('context.rendererRoot', join(this.appPath, 'renderer')))
}
get _runtimeRoot() {
return this.get('options.runtimeRoot', this.get('context.runtimeRoot', join(this.rendererRoot, 'runtime')))
}
get _mainRoot() {
return this.get('options.mainRoot', this.get('context.mainRoot', join(this.appPath, 'main')))
}
browserWindowClosed() {
delete(this.browserWindow)
}
reloadWindow(ignoreCache = false) {
return this.result(`webContents.${ ignoreCache ? 'reloadIgnoringCache' : 'reload'}`)
}
emulateMobileDevice(options = {}) {
return this.invoke('webContents.enableDeviceEmulation', {
screenPosition: 'mobile',
...options,
})
}
emulateDesktopDevice(options = {}) {
return this.invoke('webContents.enableDeviceEmulation', {
screenPosition: 'desktop',
...options,
})
}
disableEmulation() {
return this.invoke('webContents.disableDeviceEmulation')
}
async installDeveloperTools({react = true, devtron = true} = {}) {
try {
if (react) { await execute(`require('electron-react-devtools').install()`) }
if (devtron) { await execute(`require('devtron').install()`) }
return true
} catch(error) {
return false
}
}
async sendMessage(...args) {
return this.invoke('webContents.send', ...args)
}
async insertCSS(css) {
try {
const result = await this.invoke('webContents.insertCSS', css)
return result
} catch(error) {
return error
}
}
async execute(code, userGesture = true) {
try {
const result = await this.invoke('webContents.executeJavascript', code, userGesture)
return result
} catch(error) {
return error
}
}
get webContents() {
return this.get('browserWindow.webContents')
}
get isFullScreen() {
return this.result('browserWindow.isFullScreen')
}
get isFocused() {
return this.result('browserWindow.isFocused')
}
get isVisible() {
return this.result('browserWindow.isVisible')
}
get windowBounds() {
return this.result('browserWindow.getBounds')
}
get windowSize() {
return this.result('browserWindow.getSize')
}
get coordinates() {
const { x: left, y: top } = this.chain.get('windowBounds').pick('x', 'y').value()
return {
x: left,
y: top,
top,
left,
}
}
}
export default Panel
export const attach = Panel.attach
export function encodeArgs (args = {}) {
if (isEmpty(args)) { return '' }
return encodeURIComponent(JSON.stringify(args))
}
export function urlWithArgs (urlOrFile, args) {
args = encodeArgs(args)
var u
if (urlOrFile.indexOf('http') === 0 || urlOrFile.indexOf('data') === 0) {
var urlData = url.parse(urlOrFile)
var hash = urlData.hash || args && args.length > 0 ? args : undefined
u = url.format(assign(urlData, { hash: hash }))
} else { // presumably a file url
u = url.format({
protocol: 'file',
pathname: urlOrFile,
slashes: true,
hash: args && args.length > 0 ? args : undefined
})
}
return u
}