diff --git a/materialious/electron/.gitignore b/materialious/electron/.gitignore index c340937a..67a91a7c 100644 --- a/materialious/electron/.gitignore +++ b/materialious/electron/.gitignore @@ -3,6 +3,5 @@ # And the CLI then renames it app node_modules -build dist logs diff --git a/materialious/electron/build/capacitor.config.js b/materialious/electron/build/capacitor.config.js new file mode 100644 index 00000000..d5819777 --- /dev/null +++ b/materialious/electron/build/capacitor.config.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const config = { + appId: 'us.materialio.app', + appName: 'Materialious', + webDir: 'build', + bundledWebRuntime: false, + plugins: { + CapacitorHttp: { + enabled: true + } + }, +}; +exports.default = config; diff --git a/materialious/electron/build/logo.png b/materialious/electron/build/logo.png new file mode 100644 index 00000000..6f3a662c Binary files /dev/null and b/materialious/electron/build/logo.png differ diff --git a/materialious/electron/build/src/index.js b/materialious/electron/build/src/index.js new file mode 100644 index 00000000..a8bd9fb0 --- /dev/null +++ b/materialious/electron/build/src/index.js @@ -0,0 +1,61 @@ +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = require("tslib"); +const electron_1 = require("@capacitor-community/electron"); +const electron_2 = require("electron"); +const electron_is_dev_1 = tslib_1.__importDefault(require("electron-is-dev")); +const electron_unhandled_1 = tslib_1.__importDefault(require("electron-unhandled")); +const electron_updater_1 = require("electron-updater"); +const setup_1 = require("./setup"); +// Graceful handling of unhandled errors. +(0, electron_unhandled_1.default)(); +// Define our menu templates (these are optional) +const trayMenuTemplate = [new electron_2.MenuItem({ label: 'Quit App', role: 'quit' })]; +const appMenuBarMenuTemplate = [ + { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, + { role: 'viewMenu' }, +]; +// Get Config options from capacitor.config +const capacitorFileConfig = (0, electron_1.getCapacitorElectronConfig)(); +// Initialize our app. You can pass menu templates into the app here. +// const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); +const myCapacitorApp = new setup_1.ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); +// If deeplinking is enabled then we will set it up here. +if ((_a = capacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.deepLinkingEnabled) { + (0, electron_1.setupElectronDeepLinking)(myCapacitorApp, { + customProtocol: (_b = capacitorFileConfig.electron.deepLinkingCustomProtocol) !== null && _b !== void 0 ? _b : 'mycapacitorapp', + }); +} +// If we are in Dev mode, use the file watcher components. +if (electron_is_dev_1.default) { + (0, setup_1.setupReloadWatcher)(myCapacitorApp); +} +// Run Application +(async () => { + // Wait for electron app to be ready. + await electron_2.app.whenReady(); + // Security - Set Content-Security-Policy based on whether or not we are in dev mode. + (0, setup_1.setupContentSecurityPolicy)(myCapacitorApp.getCustomURLScheme()); + // Initialize our app, build windows, and load content. + await myCapacitorApp.init(); + // Check for updates if we are in a packaged app. + electron_updater_1.autoUpdater.checkForUpdatesAndNotify(); +})(); +// Handle when all of our windows are close (platforms have their own expectations). +electron_2.app.on('window-all-closed', function () { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + electron_2.app.quit(); + } +}); +// When the dock icon is clicked. +electron_2.app.on('activate', async function () { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (myCapacitorApp.getMainWindow().isDestroyed()) { + await myCapacitorApp.init(); + } +}); +// Place all ipc or other electron api calls and custom functionality under this line diff --git a/materialious/electron/build/src/preload.js b/materialious/electron/build/src/preload.js new file mode 100644 index 00000000..c817d3b7 --- /dev/null +++ b/materialious/electron/build/src/preload.js @@ -0,0 +1,4 @@ +require('./rt/electron-rt'); +////////////////////////////// +// User Defined Preload scripts below +console.log('User Preload!'); diff --git a/materialious/electron/build/src/rt/electron-plugins.js b/materialious/electron/build/src/rt/electron-plugins.js new file mode 100644 index 00000000..0141d0c9 --- /dev/null +++ b/materialious/electron/build/src/rt/electron-plugins.js @@ -0,0 +1,2 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +module.exports = {}; diff --git a/materialious/electron/build/src/rt/electron-rt.js b/materialious/electron/build/src/rt/electron-rt.js new file mode 100644 index 00000000..6260d86a --- /dev/null +++ b/materialious/electron/build/src/rt/electron-rt.js @@ -0,0 +1,68 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const crypto_1 = require("crypto"); +const electron_1 = require("electron"); +const events_1 = require("events"); +//////////////////////////////////////////////////////// +// eslint-disable-next-line @typescript-eslint/no-var-requires +const plugins = require('./electron-plugins'); +const randomId = (length = 5) => (0, crypto_1.randomBytes)(length).toString('hex'); +const contextApi = {}; +Object.keys(plugins).forEach((pluginKey) => { + Object.keys(plugins[pluginKey]) + .filter((className) => className !== 'default') + .forEach((classKey) => { + const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter((v) => v !== 'constructor'); + if (!contextApi[classKey]) { + contextApi[classKey] = {}; + } + functionList.forEach((functionName) => { + if (!contextApi[classKey][functionName]) { + contextApi[classKey][functionName] = (...args) => electron_1.ipcRenderer.invoke(`${classKey}-${functionName}`, ...args); + } + }); + // Events + if (plugins[pluginKey][classKey].prototype instanceof events_1.EventEmitter) { + const listeners = {}; + const listenersOfTypeExist = (type) => !!Object.values(listeners).find((listenerObj) => listenerObj.type === type); + Object.assign(contextApi[classKey], { + addListener(type, callback) { + const id = randomId(); + // Deduplicate events + if (!listenersOfTypeExist(type)) { + electron_1.ipcRenderer.send(`event-add-${classKey}`, type); + } + const eventHandler = (_, ...args) => callback(...args); + electron_1.ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler); + listeners[id] = { type, listener: eventHandler }; + return id; + }, + removeListener(id) { + if (!listeners[id]) { + throw new Error('Invalid id'); + } + const { type, listener } = listeners[id]; + electron_1.ipcRenderer.removeListener(`event-${classKey}-${type}`, listener); + delete listeners[id]; + if (!listenersOfTypeExist(type)) { + electron_1.ipcRenderer.send(`event-remove-${classKey}-${type}`); + } + }, + removeAllListeners(type) { + Object.entries(listeners).forEach(([id, listenerObj]) => { + if (!type || listenerObj.type === type) { + electron_1.ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener); + electron_1.ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`); + delete listeners[id]; + } + }); + }, + }); + } + }); +}); +electron_1.contextBridge.exposeInMainWorld('CapacitorCustomPlatform', { + name: 'electron', + plugins: contextApi, +}); +//////////////////////////////////////////////////////// diff --git a/materialious/electron/build/src/setup.js b/materialious/electron/build/src/setup.js new file mode 100644 index 00000000..859ca4d7 --- /dev/null +++ b/materialious/electron/build/src/setup.js @@ -0,0 +1,202 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ElectronCapacitorApp = void 0; +exports.setupReloadWatcher = setupReloadWatcher; +exports.setupContentSecurityPolicy = setupContentSecurityPolicy; +const tslib_1 = require("tslib"); +const electron_1 = require("@capacitor-community/electron"); +const chokidar_1 = tslib_1.__importDefault(require("chokidar")); +const electron_2 = require("electron"); +const electron_is_dev_1 = tslib_1.__importDefault(require("electron-is-dev")); +const electron_serve_1 = tslib_1.__importDefault(require("electron-serve")); +const electron_window_state_1 = tslib_1.__importDefault(require("electron-window-state")); +const path_1 = require("path"); +// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. +const reloadWatcher = { + debouncer: null, + ready: false, + watcher: null, +}; +function setupReloadWatcher(electronCapacitorApp) { + reloadWatcher.watcher = chokidar_1.default + .watch((0, path_1.join)(electron_2.app.getAppPath(), 'app'), { + ignored: /[/\\]\./, + persistent: true, + }) + .on('ready', () => { + reloadWatcher.ready = true; + }) + .on('all', (_event, _path) => { + if (reloadWatcher.ready) { + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = setTimeout(async () => { + electronCapacitorApp.getMainWindow().webContents.reload(); + reloadWatcher.ready = false; + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = null; + reloadWatcher.watcher = null; + setupReloadWatcher(electronCapacitorApp); + }, 1500); + } + }); +} +// Define our class to manage our app. +class ElectronCapacitorApp { + constructor(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate) { + var _a, _b; + this.MainWindow = null; + this.SplashScreen = null; + this.TrayIcon = null; + this.TrayMenuTemplate = [ + new electron_2.MenuItem({ label: 'Quit App', role: 'quit' }), + ]; + this.AppMenuBarMenuTemplate = [ + { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, + { role: 'viewMenu' }, + ]; + this.CapacitorFileConfig = capacitorFileConfig; + this.customScheme = (_b = (_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.customUrlScheme) !== null && _b !== void 0 ? _b : 'capacitor-electron'; + if (trayMenuTemplate) { + this.TrayMenuTemplate = trayMenuTemplate; + } + if (appMenuBarMenuTemplate) { + this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate; + } + // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. + this.loadWebApp = (0, electron_serve_1.default)({ + directory: (0, path_1.join)(electron_2.app.getAppPath(), 'app'), + scheme: this.customScheme, + }); + } + // Helper function to load in the app. + async loadMainWindow(thisRef) { + await thisRef.loadWebApp(thisRef.MainWindow); + } + // Expose the mainWindow ref for use outside of the class. + getMainWindow() { + return this.MainWindow; + } + getCustomURLScheme() { + return this.customScheme; + } + async init() { + var _a, _b, _c, _d; + const icon = electron_2.nativeImage.createFromPath((0, path_1.join)(electron_2.app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')); + this.mainWindowState = (0, electron_window_state_1.default)({ + defaultWidth: 1000, + defaultHeight: 800, + }); + // Setup preload script path and construct our main window. + const preloadPath = (0, path_1.join)(electron_2.app.getAppPath(), 'build', 'src', 'preload.js'); + this.MainWindow = new electron_2.BrowserWindow({ + icon, + show: false, + x: this.mainWindowState.x, + y: this.mainWindowState.y, + width: this.mainWindowState.width, + height: this.mainWindowState.height, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + webSecurity: false, + // Use preload to inject the electron varriant overrides for capacitor plugins. + // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), + preload: preloadPath, + }, + }); + this.mainWindowState.manage(this.MainWindow); + if (this.CapacitorFileConfig.backgroundColor) { + this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); + } + // If we close the main window with the splashscreen enabled we need to destory the ref. + this.MainWindow.on('closed', () => { + var _a; + if (((_a = this.SplashScreen) === null || _a === void 0 ? void 0 : _a.getSplashWindow()) && !this.SplashScreen.getSplashWindow().isDestroyed()) { + this.SplashScreen.getSplashWindow().close(); + } + }); + // When the tray icon is enabled, setup the options. + if ((_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.trayIconAndMenuEnabled) { + this.TrayIcon = new electron_2.Tray(icon); + this.TrayIcon.on('double-click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } + else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.on('click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } + else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.setToolTip(electron_2.app.getName()); + this.TrayIcon.setContextMenu(electron_2.Menu.buildFromTemplate(this.TrayMenuTemplate)); + } + // Setup the main manu bar at the top of our window. + electron_2.Menu.setApplicationMenu(electron_2.Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)); + // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. + if ((_b = this.CapacitorFileConfig.electron) === null || _b === void 0 ? void 0 : _b.splashScreenEnabled) { + this.SplashScreen = new electron_1.CapacitorSplashScreen({ + imageFilePath: (0, path_1.join)(electron_2.app.getAppPath(), 'assets', (_d = (_c = this.CapacitorFileConfig.electron) === null || _c === void 0 ? void 0 : _c.splashScreenImageName) !== null && _d !== void 0 ? _d : 'splash.png'), + windowWidth: 400, + windowHeight: 400, + }); + this.SplashScreen.init(this.loadMainWindow, this); + } + else { + this.loadMainWindow(this); + } + // Security + this.MainWindow.webContents.setWindowOpenHandler((details) => { + if (!details.url.includes(this.customScheme)) { + return { action: 'deny' }; + } + else { + return { action: 'allow' }; + } + }); + this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { + if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { + event.preventDefault(); + } + }); + // Link electron plugins into the system. + (0, electron_1.setupCapacitorElectronPlugins)(); + // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. + this.MainWindow.webContents.on('dom-ready', () => { + var _a, _b; + if ((_a = this.CapacitorFileConfig.electron) === null || _a === void 0 ? void 0 : _a.splashScreenEnabled) { + this.SplashScreen.getSplashWindow().hide(); + } + if (!((_b = this.CapacitorFileConfig.electron) === null || _b === void 0 ? void 0 : _b.hideMainWindowOnLaunch)) { + this.MainWindow.show(); + } + setTimeout(() => { + if (electron_is_dev_1.default) { + this.MainWindow.webContents.openDevTools(); + } + electron_1.CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); + }, 400); + }); + } +} +exports.ElectronCapacitorApp = ElectronCapacitorApp; +// Set a CSP up for our application based on the custom scheme +function setupContentSecurityPolicy(customScheme) { + electron_2.session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: Object.assign({}, details.responseHeaders), + }); + }); +}