From f3060f290e0a28d9eecac5a2d6aa81f57e904261 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 13 Nov 2023 17:18:58 +0700 Subject: [PATCH] refactor: main electron with managers and handlers --- electron/handlers/app.ts | 65 +++++ electron/handlers/download.ts | 106 ++++++++ electron/handlers/fs.ts | 27 +- electron/handlers/plugin.ts | 119 +++++++++ electron/handlers/theme.ts | 27 ++ electron/handlers/update.ts | 59 +++++ electron/main.ts | 459 +++------------------------------- electron/managers/download.ts | 24 ++ electron/managers/module.ts | 33 +++ electron/managers/plugin.ts | 60 +++++ electron/managers/window.ts | 37 +++ electron/package.json | 1 + electron/tsconfig.json | 1 + 13 files changed, 588 insertions(+), 430 deletions(-) create mode 100644 electron/handlers/app.ts create mode 100644 electron/handlers/download.ts create mode 100644 electron/handlers/plugin.ts create mode 100644 electron/handlers/theme.ts create mode 100644 electron/handlers/update.ts create mode 100644 electron/managers/download.ts create mode 100644 electron/managers/module.ts create mode 100644 electron/managers/plugin.ts create mode 100644 electron/managers/window.ts diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts new file mode 100644 index 000000000..022e4d61a --- /dev/null +++ b/electron/handlers/app.ts @@ -0,0 +1,65 @@ +import { app, ipcMain, shell } from "electron"; +import { ModuleManager } from "../managers/module"; +import { join } from "path"; +import { PluginManager } from "../managers/plugin"; +import { WindowManager } from "../managers/window"; + +export function handleAppIPCs() { + /** + * Retrieves the path to the app data directory using the `coreAPI` object. + * If the `coreAPI` object is not available, the function returns `undefined`. + * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. + */ + ipcMain.handle("appDataPath", async (_event) => { + return app.getPath("userData"); + }); + + /** + * Returns the version of the app. + * @param _event - The IPC event object. + * @returns The version of the app. + */ + ipcMain.handle("appVersion", async (_event) => { + return app.getVersion(); + }); + + /** + * Handles the "openAppDirectory" IPC message by opening the app's user data directory. + * The `shell.openPath` method is used to open the directory in the user's default file explorer. + * @param _event - The IPC event object. + */ + ipcMain.handle("openAppDirectory", async (_event) => { + shell.openPath(app.getPath("userData")); + }); + + /** + * Opens a URL in the user's default browser. + * @param _event - The IPC event object. + * @param url - The URL to open. + */ + ipcMain.handle("openExternalUrl", async (_event, url) => { + shell.openExternal(url); + }); + + /** + * Relaunches the app in production - reload window in development. + * @param _event - The IPC event object. + * @param url - The URL to reload. + */ + ipcMain.handle("relaunch", async (_event, url) => { + ModuleManager.instance.clearImportedModules(); + + if (app.isPackaged) { + app.relaunch(); + app.exit(); + } else { + for (const modulePath in ModuleManager.instance.requiredModules) { + delete require.cache[ + require.resolve(join(app.getPath("userData"), "plugins", modulePath)) + ]; + } + PluginManager.instance.setupPlugins(); + WindowManager.instance.currentWindow?.reload(); + } + }); +} diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts new file mode 100644 index 000000000..ab672f25c --- /dev/null +++ b/electron/handlers/download.ts @@ -0,0 +1,106 @@ +import { app, ipcMain } from "electron"; +import { DownloadManager } from "../managers/download"; +import { resolve, join } from "path"; +import { WindowManager } from "../managers/window"; +import request from "request"; +import { createWriteStream, unlink } from "fs"; +const progress = require("request-progress"); + +export function handleDownloaderIPCs() { + /** + * Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName. + * @param _event - The IPC event object. + * @param fileName - The name of the file being downloaded. + */ + ipcMain.handle("pauseDownload", async (_event, fileName) => { + DownloadManager.instance.networkRequests[fileName]?.pause(); + }); + + /** + * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName. + * @param _event - The IPC event object. + * @param fileName - The name of the file being downloaded. + */ + ipcMain.handle("resumeDownload", async (_event, fileName) => { + DownloadManager.instance.networkRequests[fileName]?.resume(); + }); + + /** + * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName. + * The network request associated with the fileName is then removed from the networkRequests object. + * @param _event - The IPC event object. + * @param fileName - The name of the file being downloaded. + */ + ipcMain.handle("abortDownload", async (_event, fileName) => { + const rq = DownloadManager.instance.networkRequests[fileName]; + DownloadManager.instance.networkRequests[fileName] = undefined; + const userDataPath = app.getPath("userData"); + const fullPath = join(userDataPath, fileName); + rq?.abort(); + let result = "NULL"; + unlink(fullPath, function (err) { + if (err && err.code == "ENOENT") { + result = `File not exist: ${err}`; + } else if (err) { + result = `File delete error: ${err}`; + } else { + result = "File deleted successfully"; + } + console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`); + }); + }); + + /** + * Downloads a file from a given URL. + * @param _event - The IPC event object. + * @param url - The URL to download the file from. + * @param fileName - The name to give the downloaded file. + */ + ipcMain.handle("downloadFile", async (_event, url, fileName) => { + const userDataPath = app.getPath("userData"); + const destination = resolve(userDataPath, fileName); + const rq = request(url); + + progress(rq, {}) + .on("progress", function (state: any) { + WindowManager?.instance.currentWindow?.webContents.send( + "FILE_DOWNLOAD_UPDATE", + { + ...state, + fileName, + } + ); + }) + .on("error", function (err: Error) { + WindowManager?.instance.currentWindow?.webContents.send( + "FILE_DOWNLOAD_ERROR", + { + fileName, + err, + } + ); + }) + .on("end", function () { + if (DownloadManager.instance.networkRequests[fileName]) { + WindowManager?.instance.currentWindow?.webContents.send( + "FILE_DOWNLOAD_COMPLETE", + { + fileName, + } + ); + DownloadManager.instance.setRequest(fileName, undefined); + } else { + WindowManager?.instance.currentWindow?.webContents.send( + "FILE_DOWNLOAD_ERROR", + { + fileName, + err: "Download cancelled", + } + ); + } + }) + .pipe(createWriteStream(destination)); + + DownloadManager.instance.setRequest(fileName, rq); + }); +} diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 96714259d..af77e3002 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -5,7 +5,7 @@ import { join } from "path"; /** * Handles file system operations. */ -export function handleFs() { +export function handleFsIPCs() { /** * Reads a file from the user data directory. * @param event - The event object. @@ -115,4 +115,29 @@ export function handleFs() { }); } ); + + /** + * Deletes a file from the user data folder. + * @param _event - The IPC event object. + * @param filePath - The path to the file to delete. + * @returns A string indicating the result of the operation. + */ + ipcMain.handle("deleteFile", async (_event, filePath) => { + const userDataPath = app.getPath("userData"); + const fullPath = join(userDataPath, filePath); + + let result = "NULL"; + fs.unlink(fullPath, function (err) { + if (err && err.code == "ENOENT") { + result = `File not exist: ${err}`; + } else if (err) { + result = `File delete error: ${err}`; + } else { + result = "File deleted successfully"; + } + console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`); + }); + + return result; + }); } diff --git a/electron/handlers/plugin.ts b/electron/handlers/plugin.ts new file mode 100644 index 000000000..4446a667d --- /dev/null +++ b/electron/handlers/plugin.ts @@ -0,0 +1,119 @@ +import { app, ipcMain } from "electron"; +import { readdirSync, rmdir, writeFileSync } from "fs"; +import { ModuleManager } from "../managers/module"; +import { join, extname } from "path"; +import { PluginManager } from "../managers/plugin"; +import { WindowManager } from "../managers/window"; +const pacote = require("pacote"); + +export function handlePluginIPCs() { + /** + * Invokes a function from a plugin module in main node process. + * @param _event - The IPC event object. + * @param modulePath - The path to the plugin module. + * @param method - The name of the function to invoke. + * @param args - The arguments to pass to the function. + * @returns The result of the invoked function. + */ + ipcMain.handle( + "invokePluginFunc", + async (_event, modulePath, method, ...args) => { + const module = require( + /* webpackIgnore: true */ join( + app.getPath("userData"), + "plugins", + modulePath + ) + ); + ModuleManager.instance.setModule(modulePath, module); + + if (typeof module[method] === "function") { + return module[method](...args); + } else { + console.log(module[method]); + console.error(`Function "${method}" does not exist in the module.`); + } + } + ); + + /** + * Returns the paths of the base plugins. + * @param _event - The IPC event object. + * @returns An array of paths to the base plugins. + */ + ipcMain.handle("basePlugins", async (_event) => { + const basePluginPath = join( + __dirname, + "../", + app.isPackaged + ? "../../app.asar.unpacked/core/pre-install" + : "../core/pre-install" + ); + return readdirSync(basePluginPath) + .filter((file) => extname(file) === ".tgz") + .map((file) => join(basePluginPath, file)); + }); + + /** + * Returns the path to the user's plugin directory. + * @param _event - The IPC event object. + * @returns The path to the user's plugin directory. + */ + ipcMain.handle("pluginPath", async (_event) => { + return join(app.getPath("userData"), "plugins"); + }); + + /** + * Deletes the `plugins` directory in the user data path and disposes of required modules. + * If the app is packaged, the function relaunches the app and exits. + * Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window. + * @param _event - The IPC event object. + * @param url - The URL to reload. + */ + ipcMain.handle("reloadPlugins", async (_event, url) => { + const userDataPath = app.getPath("userData"); + const fullPath = join(userDataPath, "plugins"); + + rmdir(fullPath, { recursive: true }, function (err) { + if (err) console.log(err); + ModuleManager.instance.clearImportedModules(); + + // just relaunch if packaged, should launch manually in development mode + if (app.isPackaged) { + app.relaunch(); + app.exit(); + } else { + for (const modulePath in ModuleManager.instance.requiredModules) { + delete require.cache[ + require.resolve( + join(app.getPath("userData"), "plugins", modulePath) + ) + ]; + } + PluginManager.instance.setupPlugins(); + WindowManager.instance.currentWindow?.reload(); + } + }); + }); + + /** + * Installs a remote plugin by downloading its tarball and writing it to a tgz file. + * @param _event - The IPC event object. + * @param pluginName - The name of the remote plugin to install. + * @returns A Promise that resolves to the path of the installed plugin file. + */ + ipcMain.handle("installRemotePlugin", async (_event, pluginName) => { + const destination = join( + app.getPath("userData"), + pluginName.replace(/^@.*\//, "") + ".tgz" + ); + return pacote + .manifest(pluginName) + .then(async (manifest: any) => { + await pacote.tarball(manifest._resolved).then((data: Buffer) => { + writeFileSync(destination, data); + }); + }) + .then(() => destination); + }); +} diff --git a/electron/handlers/theme.ts b/electron/handlers/theme.ts new file mode 100644 index 000000000..0038002a8 --- /dev/null +++ b/electron/handlers/theme.ts @@ -0,0 +1,27 @@ +import { ipcMain, nativeTheme } from "electron"; + +export function handleThemesIPCs() { + /** + * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". + * This will change the appearance of the app to the light theme. + */ + ipcMain.handle("setNativeThemeLight", () => { + nativeTheme.themeSource = "light"; + }); + + /** + * Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark". + * This will change the appearance of the app to the dark theme. + */ + ipcMain.handle("setNativeThemeDark", () => { + nativeTheme.themeSource = "dark"; + }); + + /** + * Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system". + * This will change the appearance of the app to match the system's current theme. + */ + ipcMain.handle("setNativeThemeSystem", () => { + nativeTheme.themeSource = "system"; + }); +} diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts new file mode 100644 index 000000000..ae582e88f --- /dev/null +++ b/electron/handlers/update.ts @@ -0,0 +1,59 @@ +import { app, dialog } from "electron"; +import { WindowManager } from "../managers/window"; + +const { autoUpdater } = require("electron-updater"); + +export function handleAppUpdates() { + /* Should not check for update during development */ + if (!app.isPackaged) { + return; + } + /* New Update Available */ + autoUpdater.on("update-available", async (_info: any) => { + const action = await dialog.showMessageBox({ + message: `Update available. Do you want to download the latest update?`, + buttons: ["Download", "Later"], + }); + if (action.response === 0) await autoUpdater.downloadUpdate(); + }); + + /* App Update Completion Message */ + autoUpdater.on("update-downloaded", async (_info: any) => { + WindowManager.instance.currentWindow?.webContents.send( + "APP_UPDATE_COMPLETE", + {} + ); + const action = await dialog.showMessageBox({ + message: `Update downloaded. Please restart the application to apply the updates.`, + buttons: ["Restart", "Later"], + }); + if (action.response === 0) { + autoUpdater.quitAndInstall(); + } + }); + + /* App Update Error */ + autoUpdater.on("error", (info: any) => { + dialog.showMessageBox({ message: info.message }); + WindowManager.instance.currentWindow?.webContents.send( + "APP_UPDATE_ERROR", + {} + ); + }); + + /* App Update Progress */ + autoUpdater.on("download-progress", (progress: any) => { + console.log("app update progress: ", progress.percent); + WindowManager.instance.currentWindow?.webContents.send( + "APP_UPDATE_PROGRESS", + { + percent: progress.percent, + } + ); + }); + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + if (process.env.CI !== "e2e") { + autoUpdater.checkForUpdates(); + } +} diff --git a/electron/main.ts b/electron/main.ts index 86d9d0b34..741a75867 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,33 +1,28 @@ -import { - app, - BrowserWindow, - ipcMain, - dialog, - shell, - nativeTheme, -} from "electron"; -import { readdirSync, writeFileSync } from "fs"; -import { resolve, join, extname } from "path"; -import { rmdir, unlink, createWriteStream } from "fs"; -import { init } from "./core/plugin/index"; +import { app, BrowserWindow } from "electron"; +import { join } from "path"; import { setupMenu } from "./utils/menu"; -import { dispose } from "./utils/disposable"; -import { handleFs } from "./handlers/fs"; +import { handleFsIPCs } from "./handlers/fs"; -const pacote = require("pacote"); -const request = require("request"); -const progress = require("request-progress"); -const { autoUpdater } = require("electron-updater"); -const Store = require("electron-store"); +/** + * Managers + **/ +import { WindowManager } from "./managers/window"; +import { ModuleManager } from "./managers/module"; +import { PluginManager } from "./managers/plugin"; -let requiredModules: Record = {}; -const networkRequests: Record = {}; -let mainWindow: BrowserWindow | undefined = undefined; +/** + * IPC Handlers + **/ +import { handleDownloaderIPCs } from "./handlers/download"; +import { handleThemesIPCs } from "./handlers/theme"; +import { handlePluginIPCs } from "./handlers/plugin"; +import { handleAppIPCs } from "./handlers/app"; +import { handleAppUpdates } from "./handlers/update"; app .whenReady() - .then(migratePlugins) - .then(setupPlugins) + .then(PluginManager.instance.migratePlugins) + .then(PluginManager.instance.setupPlugins) .then(setupMenu) .then(handleIPCs) .then(handleAppUpdates) @@ -41,27 +36,18 @@ app }); app.on("window-all-closed", () => { - clearImportedModules(); + ModuleManager.instance.clearImportedModules(); app.quit(); }); app.on("quit", () => { - clearImportedModules(); + ModuleManager.instance.clearImportedModules(); app.quit(); }); function createMainWindow() { - mainWindow = new BrowserWindow({ - width: 1200, - minWidth: 800, - height: 800, - show: false, - trafficLightPosition: { - x: 10, - y: 15, - }, - titleBarStyle: "hidden", - vibrancy: "sidebar", + /* Create main window */ + const mainWindow = WindowManager.instance.createWindow({ webPreferences: { nodeIntegration: true, preload: join(__dirname, "preload.js"), @@ -73,6 +59,7 @@ function createMainWindow() { ? `file://${join(__dirname, "../renderer/index.html")}` : "http://localhost:3000"; + /* Load frontend app to the window */ mainWindow.loadURL(startURL); mainWindow.once("ready-to-show", () => mainWindow?.show()); @@ -80,403 +67,17 @@ function createMainWindow() { if (process.platform !== "darwin") app.quit(); }); + /* Enable dev tools for development */ if (!app.isPackaged) mainWindow.webContents.openDevTools(); } -function handleAppUpdates() { - /*New Update Available*/ - autoUpdater.on("update-available", async (_info: any) => { - const action = await dialog.showMessageBox({ - message: `Update available. Do you want to download the latest update?`, - buttons: ["Download", "Later"], - }); - if (action.response === 0) await autoUpdater.downloadUpdate(); - }); - - /*App Update Completion Message*/ - autoUpdater.on("update-downloaded", async (_info: any) => { - mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {}); - const action = await dialog.showMessageBox({ - message: `Update downloaded. Please restart the application to apply the updates.`, - buttons: ["Restart", "Later"], - }); - if (action.response === 0) { - autoUpdater.quitAndInstall(); - } - }); - - /*App Update Error */ - autoUpdater.on("error", (info: any) => { - dialog.showMessageBox({ message: info.message }); - mainWindow?.webContents.send("APP_UPDATE_ERROR", {}); - }); - - /*App Update Progress */ - autoUpdater.on("download-progress", (progress: any) => { - console.log("app update progress: ", progress.percent); - mainWindow?.webContents.send("APP_UPDATE_PROGRESS", { - percent: progress.percent, - }); - }); - autoUpdater.autoDownload = false; - autoUpdater.autoInstallOnAppQuit = true; - if (process.env.CI !== "e2e") { - autoUpdater.checkForUpdates(); - } -} - /** * Handles various IPC messages from the renderer process. */ function handleIPCs() { - handleFs(); - /** - * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". - * This will change the appearance of the app to the light theme. - */ - ipcMain.handle("setNativeThemeLight", () => { - nativeTheme.themeSource = "light"; - }); - - /** - * Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark". - * This will change the appearance of the app to the dark theme. - */ - ipcMain.handle("setNativeThemeDark", () => { - nativeTheme.themeSource = "dark"; - }); - - /** - * Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system". - * This will change the appearance of the app to match the system's current theme. - */ - ipcMain.handle("setNativeThemeSystem", () => { - nativeTheme.themeSource = "system"; - }); - - /** - * Invokes a function from a plugin module in main node process. - * @param _event - The IPC event object. - * @param modulePath - The path to the plugin module. - * @param method - The name of the function to invoke. - * @param args - The arguments to pass to the function. - * @returns The result of the invoked function. - */ - ipcMain.handle( - "invokePluginFunc", - async (_event, modulePath, method, ...args) => { - const module = require( - /* webpackIgnore: true */ join( - app.getPath("userData"), - "plugins", - modulePath - ) - ); - requiredModules[modulePath] = module; - - if (typeof module[method] === "function") { - return module[method](...args); - } else { - console.log(module[method]); - console.error(`Function "${method}" does not exist in the module.`); - } - } - ); - - /** - * Returns the paths of the base plugins. - * @param _event - The IPC event object. - * @returns An array of paths to the base plugins. - */ - ipcMain.handle("basePlugins", async (_event) => { - const basePluginPath = join( - __dirname, - "../", - app.isPackaged - ? "../app.asar.unpacked/core/pre-install" - : "/core/pre-install" - ); - return readdirSync(basePluginPath) - .filter((file) => extname(file) === ".tgz") - .map((file) => join(basePluginPath, file)); - }); - - /** - * Returns the path to the user's plugin directory. - * @param _event - The IPC event object. - * @returns The path to the user's plugin directory. - */ - ipcMain.handle("pluginPath", async (_event) => { - return join(app.getPath("userData"), "plugins"); - }); - - /** - * Retrieves the path to the app data directory using the `coreAPI` object. - * If the `coreAPI` object is not available, the function returns `undefined`. - * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. - */ - ipcMain.handle("appDataPath", async (_event) => { - return app.getPath("userData"); - }); - - /** - * Returns the version of the app. - * @param _event - The IPC event object. - * @returns The version of the app. - */ - ipcMain.handle("appVersion", async (_event) => { - return app.getVersion(); - }); - - /** - * Handles the "openAppDirectory" IPC message by opening the app's user data directory. - * The `shell.openPath` method is used to open the directory in the user's default file explorer. - * @param _event - The IPC event object. - */ - ipcMain.handle("openAppDirectory", async (_event) => { - shell.openPath(app.getPath("userData")); - }); - - /** - * Opens a URL in the user's default browser. - * @param _event - The IPC event object. - * @param url - The URL to open. - */ - ipcMain.handle("openExternalUrl", async (_event, url) => { - shell.openExternal(url); - }); - - /** - * Relaunches the app in production - reload window in development. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle("relaunch", async (_event, url) => { - clearImportedModules(); - - if (app.isPackaged) { - app.relaunch(); - app.exit(); - } else { - for (const modulePath in requiredModules) { - delete require.cache[ - require.resolve(join(app.getPath("userData"), "plugins", modulePath)) - ]; - } - setupPlugins(); - mainWindow?.reload(); - } - }); - - /** - * Deletes the `plugins` directory in the user data path and disposes of required modules. - * If the app is packaged, the function relaunches the app and exits. - * Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle("reloadPlugins", async (_event, url) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.log(err); - clearImportedModules(); - - // just relaunch if packaged, should launch manually in development mode - if (app.isPackaged) { - app.relaunch(); - app.exit(); - } else { - for (const modulePath in requiredModules) { - delete require.cache[ - require.resolve( - join(app.getPath("userData"), "plugins", modulePath) - ) - ]; - } - setupPlugins(); - mainWindow?.reload(); - } - }); - }); - - /** - * Deletes a file from the user data folder. - * @param _event - The IPC event object. - * @param filePath - The path to the file to delete. - * @returns A string indicating the result of the operation. - */ - ipcMain.handle("deleteFile", async (_event, filePath) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, filePath); - - let result = "NULL"; - unlink(fullPath, function (err) { - if (err && err.code == "ENOENT") { - result = `File not exist: ${err}`; - } else if (err) { - result = `File delete error: ${err}`; - } else { - result = "File deleted successfully"; - } - console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`); - }); - - return result; - }); - - /** - * Downloads a file from a given URL. - * @param _event - The IPC event object. - * @param url - The URL to download the file from. - * @param fileName - The name to give the downloaded file. - */ - ipcMain.handle("downloadFile", async (_event, url, fileName) => { - const userDataPath = app.getPath("userData"); - const destination = resolve(userDataPath, fileName); - const rq = request(url); - - progress(rq, {}) - .on("progress", function (state: any) { - mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", { - ...state, - fileName, - }); - }) - .on("error", function (err: Error) { - mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", { - fileName, - err, - }); - networkRequests[fileName] = undefined; - }) - .on("end", function () { - if (networkRequests[fileName]) { - mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", { - fileName, - }); - networkRequests[fileName] = undefined; - } else { - mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", { - fileName, - err: "Download cancelled", - }); - } - }) - .pipe(createWriteStream(destination)); - - networkRequests[fileName] = rq; - }); - - /** - * Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle("pauseDownload", async (_event, fileName) => { - networkRequests[fileName]?.pause(); - }); - - /** - * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle("resumeDownload", async (_event, fileName) => { - networkRequests[fileName]?.resume(); - }); - - /** - * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName. - * The network request associated with the fileName is then removed from the networkRequests object. - * @param _event - The IPC event object. - * @param fileName - The name of the file being downloaded. - */ - ipcMain.handle("abortDownload", async (_event, fileName) => { - const rq = networkRequests[fileName]; - networkRequests[fileName] = undefined; - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, fileName); - rq?.abort(); - let result = "NULL"; - unlink(fullPath, function (err) { - if (err && err.code == "ENOENT") { - result = `File not exist: ${err}`; - } else if (err) { - result = `File delete error: ${err}`; - } else { - result = "File deleted successfully"; - } - console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`); - }); - }); - - /** - * Installs a remote plugin by downloading its tarball and writing it to a tgz file. - * @param _event - The IPC event object. - * @param pluginName - The name of the remote plugin to install. - * @returns A Promise that resolves to the path of the installed plugin file. - */ - ipcMain.handle("installRemotePlugin", async (_event, pluginName) => { - const destination = join( - app.getPath("userData"), - pluginName.replace(/^@.*\//, "") + ".tgz" - ); - return pacote - .manifest(pluginName) - .then(async (manifest: any) => { - await pacote.tarball(manifest._resolved).then((data: Buffer) => { - writeFileSync(destination, data); - }); - }) - .then(() => destination); - }); -} - -/** - * Migrates the plugins by deleting the `plugins` directory in the user data path. - * If the `migrated_version` key in the `Store` object does not match the current app version, - * the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version. - * @returns A Promise that resolves when the migration is complete. - */ -function migratePlugins() { - return new Promise((resolve) => { - const store = new Store(); - if (store.get("migrated_version") !== app.getVersion()) { - console.log("start migration:", store.get("migrated_version")); - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.log(err); - store.set("migrated_version", app.getVersion()); - console.log("migrate plugins done"); - resolve(undefined); - }); - } else { - resolve(undefined); - } - }); -} - -/** - * Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options. - * The `confirmInstall` function always returns `true` to allow plugin installation. - * The `pluginsPath` option specifies the path to install plugins to. - */ -function setupPlugins() { - init({ - // Function to check from the main process that user wants to install a plugin - confirmInstall: async (_plugins: string[]) => { - return true; - }, - // Path to install plugin to - pluginsPath: join(app.getPath("userData"), "plugins"), - }); -} - -function clearImportedModules() { - dispose(requiredModules); - requiredModules = {}; + handleFsIPCs(); + handleDownloaderIPCs(); + handleThemesIPCs(); + handlePluginIPCs(); + handleAppIPCs(); } diff --git a/electron/managers/download.ts b/electron/managers/download.ts new file mode 100644 index 000000000..08c089b74 --- /dev/null +++ b/electron/managers/download.ts @@ -0,0 +1,24 @@ +import { Request } from "request"; + +/** + * Manages file downloads and network requests. + */ +export class DownloadManager { + public networkRequests: Record = {}; + + public static instance: DownloadManager = new DownloadManager(); + + constructor() { + if (DownloadManager.instance) { + return DownloadManager.instance; + } + } + /** + * Sets a network request for a specific file. + * @param {string} fileName - The name of the file. + * @param {Request | undefined} request - The network request to set, or undefined to clear the request. + */ + setRequest(fileName: string, request: Request | undefined) { + this.networkRequests[fileName] = request; + } +} diff --git a/electron/managers/module.ts b/electron/managers/module.ts new file mode 100644 index 000000000..43dda0fb6 --- /dev/null +++ b/electron/managers/module.ts @@ -0,0 +1,33 @@ +import { dispose } from "../utils/disposable"; + +/** + * Manages imported modules. + */ +export class ModuleManager { + public requiredModules: Record = {}; + + public static instance: ModuleManager = new ModuleManager(); + + constructor() { + if (ModuleManager.instance) { + return ModuleManager.instance; + } + } + + /** + * Sets a module. + * @param {string} moduleName - The name of the module. + * @param {any | undefined} nodule - The module to set, or undefined to clear the module. + */ + setModule(moduleName: string, nodule: any | undefined) { + this.requiredModules[moduleName] = nodule; + } + + /** + * Clears all imported modules. + */ + clearImportedModules() { + dispose(this.requiredModules); + this.requiredModules = {}; + } +} diff --git a/electron/managers/plugin.ts b/electron/managers/plugin.ts new file mode 100644 index 000000000..889425ec7 --- /dev/null +++ b/electron/managers/plugin.ts @@ -0,0 +1,60 @@ +import { app } from "electron"; +import { init } from "../core/plugin/index"; +import { join } from "path"; +import { rmdir } from "fs"; +import Store from "electron-store"; + +/** + * Manages plugin installation and migration. + */ +export class PluginManager { + public static instance: PluginManager = new PluginManager(); + + constructor() { + if (PluginManager.instance) { + return PluginManager.instance; + } + } + + /** + * Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options. + * The `confirmInstall` function always returns `true` to allow plugin installation. + * The `pluginsPath` option specifies the path to install plugins to. + */ + setupPlugins() { + init({ + // Function to check from the main process that user wants to install a plugin + confirmInstall: async (_plugins: string[]) => { + return true; + }, + // Path to install plugin to + pluginsPath: join(app.getPath("userData"), "plugins"), + }); + } + + /** + * Migrates the plugins by deleting the `plugins` directory in the user data path. + * If the `migrated_version` key in the `Store` object does not match the current app version, + * the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version. + * @returns A Promise that resolves when the migration is complete. + */ + migratePlugins() { + return new Promise((resolve) => { + const store = new Store(); + if (store.get("migrated_version") !== app.getVersion()) { + console.log("start migration:", store.get("migrated_version")); + const userDataPath = app.getPath("userData"); + const fullPath = join(userDataPath, "plugins"); + + rmdir(fullPath, { recursive: true }, function (err) { + if (err) console.log(err); + store.set("migrated_version", app.getVersion()); + console.log("migrate plugins done"); + resolve(undefined); + }); + } else { + resolve(undefined); + } + }); + } +} diff --git a/electron/managers/window.ts b/electron/managers/window.ts new file mode 100644 index 000000000..c930dd5ec --- /dev/null +++ b/electron/managers/window.ts @@ -0,0 +1,37 @@ +import { BrowserWindow } from "electron"; + +/** + * Manages the current window instance. + */ +export class WindowManager { + public static instance: WindowManager = new WindowManager(); + public currentWindow?: BrowserWindow; + + constructor() { + if (WindowManager.instance) { + return WindowManager.instance; + } + } + + /** + * Creates a new window instance. + * @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with. + * @returns The created window instance. + */ + createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) { + this.currentWindow = new BrowserWindow({ + width: 1200, + minWidth: 800, + height: 800, + show: false, + trafficLightPosition: { + x: 10, + y: 15, + }, + titleBarStyle: "hidden", + vibrancy: "sidebar", + ...options, + }); + return this.currentWindow; + } +} diff --git a/electron/package.json b/electron/package.json index da9b34e38..107264805 100644 --- a/electron/package.json +++ b/electron/package.json @@ -67,6 +67,7 @@ }, "dependencies": { "@npmcli/arborist": "^7.1.0", + "@types/request": "^2.48.12", "@uiball/loaders": "^1.3.0", "electron-store": "^8.1.0", "electron-updater": "^6.1.4", diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 8276542b2..3cc218f93 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -8,6 +8,7 @@ "outDir": "./build", "rootDir": "./", "noEmitOnError": true, + "esModuleInterop": true, "baseUrl": ".", "allowJs": true, "skipLibCheck": true,