From 2097f42efb2cdc9a67adf8907e0aa48ef1b4f79f Mon Sep 17 00:00:00 2001 From: Su-Yong Date: Sun, 12 Nov 2023 01:16:34 +0900 Subject: [PATCH] feat(plugin): support dynamic plugin load / unload --- src/index.ts | 30 +++++++++++++++++++++++++--- src/loader/main.ts | 2 +- src/loader/preload.ts | 2 +- src/loader/renderer.ts | 2 +- src/plugins/ambient-mode/renderer.ts | 16 ++++++++++++--- src/plugins/utils/main/css.ts | 18 +++++++++++++++-- src/preload.ts | 17 +++++++++++++--- src/renderer.ts | 15 +++++++++++++- 8 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index d9f9fa82..440bb63e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,13 @@ import { pluginBuilders } from 'virtual:PluginBuilders'; import youtubeMusicCSS from './youtube-music.css?inline'; -import { getAllLoadedMainPlugins, loadAllMainPlugins, registerMainPlugin } from './loader/main'; +import { + forceLoadMainPlugin, + forceUnloadMainPlugin, + getAllLoadedMainPlugins, + loadAllMainPlugins, + registerMainPlugin +} from './loader/main'; import { MainPluginFactory, PluginBaseConfig, PluginBuilder } from './plugins/utils/builder'; // Catch errors and log them @@ -90,6 +96,7 @@ function onClosed() { ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins)); + const initHook = (win: BrowserWindow) => { ipcMain.handle('get-config', (_, id: keyof PluginBuilderList) => deepmerge(pluginBuilders[id].config, config.get(`plugins.${id}`) ?? {}) as PluginBuilderList[typeof id]['config']); ipcMain.handle('set-config', (_, name: string, obj: object) => config.setPartial(`plugins.${name}`, obj)); @@ -102,10 +109,27 @@ const initHook = (win: BrowserWindow) => { const isEqual = deepEqual(oldPluginConfigList[id], newPluginConfig); if (!isEqual) { - const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig); + const oldConfig = oldPluginConfigList[id] as PluginBaseConfig; + const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig) as PluginBaseConfig; + + if (config.enabled !== oldConfig.enabled) { + if (config.enabled) { + win.webContents.send('plugin:enable', id); + ipcMain.emit('plugin:enable', id); + forceLoadMainPlugin(id as keyof PluginBuilderList, win); + } else { + win.webContents.send('plugin:unload', id); + ipcMain.emit('plugin:unload', id); + forceUnloadMainPlugin(id as keyof PluginBuilderList, win); + } + } const mainPlugin = getAllLoadedMainPlugins()[id]; - if (mainPlugin) mainPlugin.onConfigChange?.(config as PluginBaseConfig); + if (mainPlugin) { + if (config.enabled) { + mainPlugin.onConfigChange?.(config); + } + } win.webContents.send('config-changed', id, config); } diff --git a/src/loader/main.ts b/src/loader/main.ts index 7897f9e2..883feead 100644 --- a/src/loader/main.ts +++ b/src/loader/main.ts @@ -37,7 +37,7 @@ const createContext = < }, }); -const forceUnloadMainPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => { +export const forceUnloadMainPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => { unregisterStyleMap[id]?.forEach((unregister) => unregister()); delete unregisterStyleMap[id]; diff --git a/src/loader/preload.ts b/src/loader/preload.ts index 7d55942c..dc598a1e 100644 --- a/src/loader/preload.ts +++ b/src/loader/preload.ts @@ -24,7 +24,7 @@ const createContext = < }, }); -const forceUnloadPreloadPlugin = (id: keyof PluginBuilderList) => { +export const forceUnloadPreloadPlugin = (id: keyof PluginBuilderList) => { unregisterStyleMap[id]?.forEach((unregister) => unregister()); delete unregisterStyleMap[id]; diff --git a/src/loader/renderer.ts b/src/loader/renderer.ts index 5c11e778..d19b3a88 100644 --- a/src/loader/renderer.ts +++ b/src/loader/renderer.ts @@ -31,7 +31,7 @@ const createContext = < }, }); -const forceUnloadRendererPlugin = (id: keyof PluginBuilderList) => { +export const forceUnloadRendererPlugin = (id: keyof PluginBuilderList) => { unregisterStyleMap[id]?.forEach((unregister) => unregister()); delete unregisterStyleMap[id]; diff --git a/src/plugins/ambient-mode/renderer.ts b/src/plugins/ambient-mode/renderer.ts index 105b9a24..11e462b8 100644 --- a/src/plugins/ambient-mode/renderer.ts +++ b/src/plugins/ambient-mode/renderer.ts @@ -11,12 +11,12 @@ export default builder.createRenderer(async ({ getConfig }) => { let opacity = initConfigData.opacity; let isFullscreen = initConfigData.fullscreen; + let unregister: (() => void) | null = null; let update: (() => void) | null = null; + let observer: MutationObserver; return { onLoad() { - let unregister: (() => void) | null = null; - const injectBlurVideo = (): (() => void) | null => { const songVideo = document.querySelector('#song-video'); const video = document.querySelector('#song-video .html5-video-container > video'); @@ -84,7 +84,6 @@ export default builder.createRenderer(async ({ getConfig }) => { blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`); blurCanvas.style.setProperty('--blur', `${blur}px`); blurCanvas.style.setProperty('--opacity', `${opacity}`); - console.log('updated!!!'); }; update = applyVideoAttributes; @@ -140,6 +139,12 @@ export default builder.createRenderer(async ({ getConfig }) => { const playerPage = document.querySelector('#player-page'); const ytmusicAppLayout = document.querySelector('#layout'); + const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open'); + if (isPageOpen) { + unregister?.(); + unregister = injectBlurVideo() ?? null; + } + const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes') { @@ -170,5 +175,10 @@ export default builder.createRenderer(async ({ getConfig }) => { update?.(); }, + onUnload() { + observer?.disconnect(); + update = null; + unregister?.(); + } }; }); diff --git a/src/plugins/utils/main/css.ts b/src/plugins/utils/main/css.ts index 11981988..a69f5f85 100644 --- a/src/plugins/utils/main/css.ts +++ b/src/plugins/utils/main/css.ts @@ -2,9 +2,16 @@ import fs from 'node:fs'; type Unregister = () => void; +let isLoaded = false; + const cssToInject = new Map void) | undefined>(); const cssToInjectFile = new Map void) | undefined>(); -export const injectCSS = (webContents: Electron.WebContents, css: string): Promise => { +export const injectCSS = async (webContents: Electron.WebContents, css: string): Promise => { + if (isLoaded) { + const key = await webContents.insertCSS(css); + return async () => await webContents.removeInsertedCSS(key); + } + return new Promise((resolve) => { if (cssToInject.size === 0 && cssToInjectFile.size === 0) { setupCssInjection(webContents); @@ -13,7 +20,12 @@ export const injectCSS = (webContents: Electron.WebContents, css: string): Promi }); }; -export const injectCSSAsFile = (webContents: Electron.WebContents, filepath: string): Promise => { +export const injectCSSAsFile = async (webContents: Electron.WebContents, filepath: string): Promise => { + if (isLoaded) { + const key = await webContents.insertCSS(fs.readFileSync(filepath, 'utf-8')); + return async () => await webContents.removeInsertedCSS(key); + } + return new Promise((resolve) => { if (cssToInject.size === 0 && cssToInjectFile.size === 0) { setupCssInjection(webContents); @@ -25,6 +37,8 @@ export const injectCSSAsFile = (webContents: Electron.WebContents, filepath: str const setupCssInjection = (webContents: Electron.WebContents) => { webContents.on('did-finish-load', () => { + isLoaded = true; + cssToInject.forEach(async (callback, css) => { const key = await webContents.insertCSS(css); const remove = async () => await webContents.removeInsertedCSS(key); diff --git a/src/preload.ts b/src/preload.ts index ea8f138a..95f2d89b 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -3,8 +3,6 @@ import is from 'electron-is'; import { pluginBuilders } from 'virtual:PluginBuilders'; -import { deepmerge } from 'deepmerge-ts'; - import config from './config'; // eslint-disable-next-line import/order @@ -14,7 +12,12 @@ import { PluginBuilder, PreloadPluginFactory } from './plugins/utils/builder'; -import { loadAllPreloadPlugins, registerPreloadPlugin } from './loader/preload'; +import { + forceLoadPreloadPlugin, + forceUnloadPreloadPlugin, + loadAllPreloadPlugins, + registerPreloadPlugin +} from './loader/preload'; Object.entries(pluginBuilders).forEach(([id, builder]) => { const typedBuilder = builder as PluginBuilder; @@ -24,6 +27,14 @@ Object.entries(pluginBuilders).forEach(([id, builder]) => { }); loadAllPreloadPlugins(); +ipcRenderer.on('plugin:unload', (_, id: keyof PluginBuilderList) => { + forceUnloadPreloadPlugin(id); +}); +ipcRenderer.on('plugin:enable', (_, id: keyof PluginBuilderList) => { + forceLoadPreloadPlugin(id); +}); + + contextBridge.exposeInMainWorld('mainConfig', config); contextBridge.exposeInMainWorld('electronIs', is); contextBridge.exposeInMainWorld('ipcRenderer', { diff --git a/src/renderer.ts b/src/renderer.ts index 9478d841..ec9f1743 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -8,7 +8,13 @@ import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugin import { startingPages } from './providers/extracted-data'; import { setupSongControls } from './providers/song-controls-front'; import setupSongInfo from './providers/song-info-front'; -import { getAllLoadedRendererPlugins, loadAllRendererPlugins, registerRendererPlugin } from './loader/renderer'; +import { + forceLoadRendererPlugin, + forceUnloadRendererPlugin, + getAllLoadedRendererPlugins, + loadAllRendererPlugins, + registerRendererPlugin +} from './loader/renderer'; let api: Element | null = null; @@ -103,6 +109,13 @@ function onApiLoaded() { }); await loadAllRendererPlugins(); + window.ipcRenderer.on('plugin:unload', (_event, id: keyof PluginBuilderList) => { + forceUnloadRendererPlugin(id); + }); + window.ipcRenderer.on('plugin:enable', (_event, id: keyof PluginBuilderList) => { + forceLoadRendererPlugin(id); + }); + window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => { const plugin = getAllLoadedRendererPlugins()[id];