UNPKG

skypager-project-types-electron-app

Version:

skypager electron app project type

824 lines (669 loc) 21.3 kB
/** * @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 }