From 1ee576bfb270fd7acc73f645b609b70b7707614f Mon Sep 17 00:00:00 2001 From: Samuele de Tomasi Date: Thu, 15 Jul 2021 18:36:35 +0200 Subject: [PATCH] Added IPC (for IpcRenderer and IpcMain) and a (very simple) loading screen --- electron/IPC/General/IPC.ts | 32 ++++++++ electron/IPC/General/channelsInterface.ts | 9 +++ electron/IPC/General/contextBridge.ts | 54 +++++++++++++ electron/IPC/systemInfo.ts | 38 +++++++++ electron/IPC/updaterInfo.ts | 94 ++++++++++++++++++++++ electron/configureDev.ts | 54 +++++++++++++ electron/index.ts | 74 ++++------------- electron/mainWindow.ts | 93 +++++++++++++++++++++ electron/preload.ts | 6 ++ package.json | 2 +- svelte/src/components/InfoElectron.svelte | 27 +++++++ svelte/src/components/Version.svelte | 94 ++++++++++++++++++++++ svelte/src/routes/help/index.svelte | 5 +- svelte/src/routes/help/tab/command.svelte | 4 +- svelte/src/routes/help/tab/renderer.svelte | 10 +++ svelte/src/routes/help/tab/start.svelte | 9 +++ svelte/static/loading.html | 1 + 17 files changed, 545 insertions(+), 61 deletions(-) create mode 100644 electron/IPC/General/IPC.ts create mode 100644 electron/IPC/General/channelsInterface.ts create mode 100644 electron/IPC/General/contextBridge.ts create mode 100644 electron/IPC/systemInfo.ts create mode 100644 electron/IPC/updaterInfo.ts create mode 100644 electron/configureDev.ts create mode 100644 electron/mainWindow.ts create mode 100644 electron/preload.ts create mode 100644 svelte/src/components/InfoElectron.svelte create mode 100644 svelte/src/components/Version.svelte create mode 100644 svelte/src/routes/help/tab/renderer.svelte create mode 100644 svelte/static/loading.html diff --git a/electron/IPC/General/IPC.ts b/electron/IPC/General/IPC.ts new file mode 100644 index 0000000..6e48710 --- /dev/null +++ b/electron/IPC/General/IPC.ts @@ -0,0 +1,32 @@ +import { BrowserWindow, IpcMain } from "electron"; +import { APIChannels, SendChannels } from "./channelsInterface"; + +export default class IPC { + nameAPI: string = "api"; + validSendChannel: SendChannels = {}; + validReceiveChannel: string[] = []; + + constructor(channels: APIChannels) { + this.nameAPI = channels.nameAPI; + this.validSendChannel = channels.validSendChannel; + this.validReceiveChannel = channels.validReceiveChannel; + } + + get channels():APIChannels { + return { + nameAPI: this.nameAPI, + validSendChannel: this.validSendChannel, + validReceiveChannel: this.validReceiveChannel + } + } + + initIpcMain(ipcMain:IpcMain, mainWindow: BrowserWindow) { + if (mainWindow) { + Object.keys(this.validSendChannel).forEach(key => { + ipcMain.on(key, async( event, message) => { + this.validSendChannel[key](mainWindow, event, message); + }); + }); + } + } +} \ No newline at end of file diff --git a/electron/IPC/General/channelsInterface.ts b/electron/IPC/General/channelsInterface.ts new file mode 100644 index 0000000..d9cff2b --- /dev/null +++ b/electron/IPC/General/channelsInterface.ts @@ -0,0 +1,9 @@ +export interface APIChannels { + nameAPI: string, + validSendChannel: SendChannels, + validReceiveChannel: string[] +} + +export interface SendChannels { + [key: string]: Function +} \ No newline at end of file diff --git a/electron/IPC/General/contextBridge.ts b/electron/IPC/General/contextBridge.ts new file mode 100644 index 0000000..bf80faa --- /dev/null +++ b/electron/IPC/General/contextBridge.ts @@ -0,0 +1,54 @@ +import { contextBridge, ipcRenderer } from "electron"; +import { APIChannels } from "./channelsInterface"; +import IPC from "./IPC"; + +interface APIContextBridge { + send: (channel: string, data: any) => void; + receive: (channel: string, func: (arg0: any) => void) => void; +} + +export function generateContextBridge(listIPC: IPC[]) { + + let listChannels: APIChannels[] = []; + listIPC.forEach(el => { + listChannels.push(el.channels); + }); + + let listAPI: {[key: string]: APIContextBridge} = {}; + + listChannels.forEach(el => { + const api = getContextBridge(el); + const name = el.nameAPI; + listAPI[name] = {...api}; + }); + + contextBridge.exposeInMainWorld("api", { + ...listAPI + }); +} + +function getContextBridge(obj: APIChannels): APIContextBridge { + const { validReceiveChannel } = { ...obj }; + const validSendChannel = getArrayOfValidSendChannel(obj); + + return { + send: (channel: string, data: any) => { + // whitelist channels + if (validSendChannel.includes(channel)) { + ipcRenderer.send(channel, data); + } + }, + receive: (channel: string, func: (arg0: any) => void) => { + if (validReceiveChannel.includes(channel)) { + // Deliberately strip event as it includes `sender` + ipcRenderer.on(channel, (event, ...args: [any]) => {func(...args);}); + } + } + } +}; + +function getArrayOfValidSendChannel(obj: APIChannels): string[] { + const { validSendChannel } = { ...obj }; + let result: string[] = Object.keys(validSendChannel); + return result; +} diff --git a/electron/IPC/systemInfo.ts b/electron/IPC/systemInfo.ts new file mode 100644 index 0000000..638c2fb --- /dev/null +++ b/electron/IPC/systemInfo.ts @@ -0,0 +1,38 @@ +import { SendChannels } from "./General/channelsInterface"; +import IPC from "./General/IPC"; +import { BrowserWindow } from "electron"; + +const nameAPI = "systemInfo"; + +// to Main +const validSendChannel: SendChannels = { + "requestSystemInfo": requestSystemInfo +}; + +// from Main +const validReceiveChannel: string[] = [ + "getSystemInfo", +]; + +const systemInfo = new IPC ({ + nameAPI, + validSendChannel, + validReceiveChannel +}); + +export default systemInfo; + +// Enter here the functions for ElectronJS + +function requestSystemInfo(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) { + const versionChrome = process.versions.chrome; + const versionNode = process.versions.node; + const versionElectron = process.versions.electron; + const result = { + chrome: versionChrome, + node: versionNode, + electron: versionElectron + } + mainWindow.webContents.send("getSystemInfo", result); +} + diff --git a/electron/IPC/updaterInfo.ts b/electron/IPC/updaterInfo.ts new file mode 100644 index 0000000..4d9b850 --- /dev/null +++ b/electron/IPC/updaterInfo.ts @@ -0,0 +1,94 @@ +import { BrowserWindow, app } from "electron"; +import { AppUpdater, autoUpdater } from "electron-updater"; +import { SendChannels } from "./General/channelsInterface"; +import IPC from "./General/IPC"; + +const nameAPI = "updaterInfo"; + +// to Main +const validSendChannel: SendChannels = { + "requestVersionNumber": requestVersionNumber, + "checkForUpdate": checkForUpdate, + "startDownloadUpdate": startDownloadUpdate, + "quitAndInstall": quitAndInstall, +}; + +// from Main +const validReceiveChannel: string[] = [ + "getVersionNumber", + "checkingForUpdate", + "updateAvailable", + "updateNotAvailable", + "downloadProgress", + "updateDownloaded", +]; + +class UpdaterInfo extends IPC { + initAutoUpdater(autoUpdater: AppUpdater, mainWindow: BrowserWindow) { + initAutoUpdater(autoUpdater, mainWindow); + } +} + +const updaterInfo = new UpdaterInfo ({ + nameAPI, + validSendChannel, + validReceiveChannel +}); + +export default updaterInfo; + +// Enter here the functions for ElectronJS + +function initAutoUpdater(autoUpdater: AppUpdater, mainWindow: BrowserWindow) { + autoUpdater.on('checking-for-update', () => { + mainWindow.webContents.send("checkingForUpdate", null); + }); + + autoUpdater.on('error', (err) => { }); + + autoUpdater.on("update-available", (info: any) => { + mainWindow.webContents.send("updateAvailable", info); + }); + + autoUpdater.on('download-progress', (info: any) => { + mainWindow.webContents.send("downloadProgress", info); + }); + + autoUpdater.on("update-downloaded", (info: any) => { + mainWindow.webContents.send("updateDownloaded", info); + }); + + autoUpdater.on("update-not-available", (info: any) => { + mainWindow.webContents.send("updateNotAvailable", info); + }); +} + +function requestVersionNumber(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) { + const version = app.getVersion(); + const result = {version}; + mainWindow.webContents.send("getVersionNumber", result); +} + +function checkForUpdate(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) { + autoUpdater.autoDownload = false; + autoUpdater.checkForUpdates(); +} + +function startDownloadUpdate(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) { + autoUpdater.downloadUpdate(); +} + +function quitAndInstall(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) { + autoUpdater.quitAndInstall(); +} + + + + + + + + + + + diff --git a/electron/configureDev.ts b/electron/configureDev.ts new file mode 100644 index 0000000..f1eba83 --- /dev/null +++ b/electron/configureDev.ts @@ -0,0 +1,54 @@ +import path from "path"; +import serve from "electron-serve"; +import { exec } from "child_process"; + +export interface DeveloperOptions { + isInProduction: boolean; + serveSvelteDev: boolean; + buildSvelteDev: boolean; + watchSvelteBuild: boolean; +} + +class ConfigureDev { + isInProduction: boolean; + serveSvelteDev: boolean; + buildSvelteDev: boolean; + watchSvelteBuild: boolean; + loadURL: any; + + constructor(settings: DeveloperOptions) { + this.isInProduction = settings.isInProduction + this.serveSvelteDev = settings.serveSvelteDev + this.buildSvelteDev = settings.buildSvelteDev + this.watchSvelteBuild = settings.watchSvelteBuild + this.loadURL = null; + + this._check_isInProduction(); + + if (!this.isInProduction && this.serveSvelteDev) this._dev_Svelte(); + if (!this.isInProduction && this.buildSvelteDev) this._build_Dist(); + if (!this.isInProduction && this.watchSvelteBuild) this._watch_Dist(); + if (this.isInProduction || !this.serveSvelteDev) this._serve_Dist(); + } + + _check_isInProduction() { + if (! this.isInProduction){ + this.isInProduction = process.env.NODE_ENV === "production" || !/[\\/]electron/.exec(process.execPath); // !process.execPath.match(/[\\/]electron/); + }; + } + _dev_Svelte() { + exec("npm run svelte:dev"); + require("electron-reload")(path.join(__dirname, "..", "svelte")); + } + _build_Dist() { exec("npm run svelte:build"); } + _watch_Dist() { require("electron-reload")(path.join(__dirname, "www")); } + _serve_Dist() { + this.loadURL = serve({ directory: "dist/www" }); + } + + isLocalHost() { return this.serveSvelteDev; } + isElectronServe() { return !this.serveSvelteDev; } + +} + +export default ConfigureDev; \ No newline at end of file diff --git a/electron/index.ts b/electron/index.ts index e3908b9..e6ef963 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -1,70 +1,30 @@ -import { app, BrowserWindow } from "electron"; -import path from "path"; -import serve from "electron-serve"; -import { exec } from "child_process"; +import { ipcMain } from 'electron'; +import { autoUpdater } from "electron-updater"; +import Main from "./mainWindow"; + +import systemInfo from './IPC/systemInfo'; +import updaterInfo from './IPC/updaterInfo'; const developerOptions = { - isInProduction: false, // true if is in production + isInProduction: true, // true if is in production serveSvelteDev: false, // true when you want to watch svelte - buildSvelteDiv: false, // true when you want to build svelte + buildSvelteDev: false, // true when you want to build svelte watchSvelteBuild: false, // true when you want to watch build svelte }; -if (! developerOptions.isInProduction){ - developerOptions.isInProduction = process.env.NODE_ENV === "production" || !/[\\/]electron/.exec(process.execPath); // !process.execPath.match(/[\\/]electron/); -} -let loadURL:any = null; - -if (!developerOptions.isInProduction && developerOptions.serveSvelteDev) { - console.log("npm run svelte:dev"); - exec("npm run svelte:dev"); - console.log("electron-reload svelte dev"); - require("electron-reload")(path.join(__dirname, "..", "svelte")); -} - -if (!developerOptions.isInProduction && developerOptions.buildSvelteDiv) { - console.log("npm run svelte:build"); - exec("npm run svelte:build"); +const windowSettings = { + title: "MEMENTO - SvelteKit, Electron, TypeScript", + width: 854, + height: 854 } -if (!developerOptions.isInProduction && developerOptions.watchSvelteBuild) { - console.log("electron-reload www"); - require("electron-reload")(path.join(__dirname, "www")); -} +let main = new Main(windowSettings, developerOptions); +main.onEvent.on("window-created", async () => { + systemInfo.initIpcMain(ipcMain, main.window); + updaterInfo.initIpcMain(ipcMain, main.window); -if (developerOptions.isInProduction || !developerOptions.serveSvelteDev) { - console.log("serve dist/www"); - loadURL = serve({ directory: "dist/www" }); -} + updaterInfo.initAutoUpdater(autoUpdater, main.window); -let mainWindow = null; - -const createWindow = async () => { - mainWindow = new BrowserWindow({ - width: 854, - height: 480, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - enableRemoteModule: true, - }, - }); - - if (developerOptions.serveSvelteDev) { - mainWindow.loadURL("http://localhost:3000/"); - } else if (loadURL) { - await loadURL(mainWindow); - } -}; - -app.on("ready", async () => { - app.name = "Svelte Template"; - await createWindow(); }); -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - } -}); diff --git a/electron/mainWindow.ts b/electron/mainWindow.ts new file mode 100644 index 0000000..1319b45 --- /dev/null +++ b/electron/mainWindow.ts @@ -0,0 +1,93 @@ +import { app, BrowserWindow } from 'electron'; +import path from "path"; +import EventEmitter from 'events'; +import ConfigureDev from './configureDev'; +import { DeveloperOptions } from "./configureDev"; + +const appName = "MEMENTO - SvelteKit, Electron, TypeScript"; + +const defaultSettings = { + title: "MEMENTO - SvelteKit, Electron, TypeScript", + width: 854, + height: 480 +} + +const defaultSettingsDev:DeveloperOptions = { + isInProduction: true, // true if is in production + serveSvelteDev: false, // true when you want to watch svelte + buildSvelteDev: false, // true when you want to build svelte + watchSvelteBuild: false, // true when you want to watch build svelte +} + +class Main { + window!: BrowserWindow; + settings: {[key: string]: any}; + onEvent: EventEmitter = new EventEmitter(); + settingsDev: DeveloperOptions; + configDev: ConfigureDev; + + constructor(settings: {[key: string]: any} | null = null, settingsDev: DeveloperOptions | null = null) { + this.settings = settings ? {...settings} : {...defaultSettings}; + this.settingsDev = settingsDev ? {...settingsDev} : {...defaultSettingsDev}; + + this.configDev = new ConfigureDev(this.settingsDev); + + app.on('ready', () => { + + let loading = new BrowserWindow({show: false, frame: false}) + + loading.once('show', async () => { + this.window = await this.createWindow(); + this.onEvent.emit("window-created"); + loading.hide() + loading.close() + }) + loading.loadURL(path.join(__dirname, "www", "loading.html")); + loading.show(); + }) + + app.on('window-all-closed', this.onWindowAllClosed); + app.on('activate', this.onActivate); + + } + + async createWindow() { + let settings = {...this.settings} + app.name = appName; + let window = new BrowserWindow({ + ...settings, + show: false, // false + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + enableRemoteModule: true, + preload: path.join(__dirname, "preload.js") + } + }); + + if (this.configDev.isLocalHost()) { + await window.loadURL("http://localhost:3000/"); + } else if (this.configDev.isElectronServe()) { + await this.configDev.loadURL(window); + } + + window.show(); + + return window; + } + + onWindowAllClosed() { + if (process.platform !== 'darwin') { + app.quit(); + } + } + + onActivate() { + if (!this.window) { + this.createWindow(); + } + } + +} + +export default Main; diff --git a/electron/preload.ts b/electron/preload.ts new file mode 100644 index 0000000..07c5214 --- /dev/null +++ b/electron/preload.ts @@ -0,0 +1,6 @@ +import { generateContextBridge } from "./IPC/General/contextBridge" + +import systemInfo from "./IPC/systemInfo"; +import updaterInfo from './IPC/updaterInfo'; + +generateContextBridge([systemInfo, updaterInfo]); diff --git a/package.json b/package.json index 1d479f2..d93cb79 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Memento: how to use Svelte with Electron and TypeScript", "author": "Samuele de Tomasi ", "license": "MIT", - "version": "0.0.1", + "version": "0.0.2", "main": "dist/index.js", "scripts": { "nodemon": "nodemon", diff --git a/svelte/src/components/InfoElectron.svelte b/svelte/src/components/InfoElectron.svelte new file mode 100644 index 0000000..cddf3a7 --- /dev/null +++ b/svelte/src/components/InfoElectron.svelte @@ -0,0 +1,27 @@ + + +
+

We are using

+
    +
  • Node.js {node}
  • +
  • Chromium {chrome}
  • +
  • Electron {electron}
  • +
+
+ + diff --git a/svelte/src/components/Version.svelte b/svelte/src/components/Version.svelte new file mode 100644 index 0000000..224f3ae --- /dev/null +++ b/svelte/src/components/Version.svelte @@ -0,0 +1,94 @@ + + +
+ App version {version}. +
+ +
+ {#if !checkingForUpdate && !updateAvailable && !downloading && !quitAndInstall} +

+ {/if} + {#if checkingForUpdate} +

Checking for update...

+ {/if} + {#if updateAvailable} +

+ {/if} + {#if updateNotAvailable} +

Update not available

+ {/if} + {#if downloading} + {downloadMessage} + {/if} + {#if quitAndInstall} +

+ {/if} +
+ + diff --git a/svelte/src/routes/help/index.svelte b/svelte/src/routes/help/index.svelte index 2e152bc..d1534f5 100644 --- a/svelte/src/routes/help/index.svelte +++ b/svelte/src/routes/help/index.svelte @@ -5,6 +5,7 @@ import Start from './tab/start.svelte'; import Command from './tab/command.svelte'; import Note from './tab/note.svelte'; + import Renderer from './tab/renderer.svelte'; const tabs: { label: string; @@ -12,7 +13,8 @@ }[] = [ { label: 'Get Started', page: Start }, { label: 'Command', page: Command }, - { label: 'Notes', page: Note } + { label: 'Notes', page: Note }, + { label: 'Renderer', page: Renderer } ]; @@ -26,6 +28,7 @@

Template to create desktop application with SvelteKit, Electron and TypeScript

+ diff --git a/svelte/src/routes/help/tab/command.svelte b/svelte/src/routes/help/tab/command.svelte index 5807e0c..008617d 100644 --- a/svelte/src/routes/help/tab/command.svelte +++ b/svelte/src/routes/help/tab/command.svelte @@ -9,12 +9,12 @@ - You can configure settings in index.ts. Change developerOptions: + You can configure settings inindex.ts. Change developerOptions:
  • isInProduction: true if is in production
  • serveSvelteDev: true when you want to watch svelte
  • -
  • buildSvelteDiv: true when you want to build svelte
  • +
  • buildSvelteDev: true when you want to build svelte
  • watchSvelteBuild: true when you want to watch build svelte
diff --git a/svelte/src/routes/help/tab/renderer.svelte b/svelte/src/routes/help/tab/renderer.svelte new file mode 100644 index 0000000..7173cc3 --- /dev/null +++ b/svelte/src/routes/help/tab/renderer.svelte @@ -0,0 +1,10 @@ + + +
+

Renderer

+ + +
diff --git a/svelte/src/routes/help/tab/start.svelte b/svelte/src/routes/help/tab/start.svelte index ed17513..205fede 100644 --- a/svelte/src/routes/help/tab/start.svelte +++ b/svelte/src/routes/help/tab/start.svelte @@ -7,6 +7,8 @@ >.

+

This project is still under development: the todos section doesn't work!

+

To create a new project based on this template using degit:

@@ -24,3 +26,10 @@ >

+ + diff --git a/svelte/static/loading.html b/svelte/static/loading.html new file mode 100644 index 0000000..3f68feb --- /dev/null +++ b/svelte/static/loading.html @@ -0,0 +1 @@ +loading \ No newline at end of file