diff --git a/src/index.ts b/src/index.ts index ee0a8062..d9f9fa82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,7 @@ import { pluginBuilders } from 'virtual:PluginBuilders'; import youtubeMusicCSS from './youtube-music.css?inline'; -import { getLoadedAllPlugins, loadAllPlugins, registerMainPlugin } from './loader/main'; +import { getAllLoadedMainPlugins, loadAllMainPlugins, registerMainPlugin } from './loader/main'; import { MainPluginFactory, PluginBaseConfig, PluginBuilder } from './plugins/utils/builder'; // Catch errors and log them @@ -104,7 +104,7 @@ const initHook = (win: BrowserWindow) => { if (!isEqual) { const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig); - const mainPlugin = getLoadedAllPlugins()[id]; + const mainPlugin = getAllLoadedMainPlugins()[id]; if (mainPlugin) mainPlugin.onConfigChange?.(config as PluginBaseConfig); win.webContents.send('config-changed', id, config); @@ -125,7 +125,7 @@ function initTheme(win: BrowserWindow) { injectCSSAsFile(win.webContents, cssFile); }, () => { - console.warn(`CSS file "${cssFile}" does not exist, ignoring`); + console.warn('[YTMusic]', `CSS file "${cssFile}" does not exist, ignoring`); }, ); } @@ -186,7 +186,7 @@ async function createMainWindow() { registerMainPlugin(id, typedBuilder, plugin); }); - await loadAllPlugins(win); + await loadAllMainPlugins(win); if (windowPosition) { const { x: windowX, y: windowY } = windowPosition; diff --git a/src/loader/main.ts b/src/loader/main.ts index 79424623..7897f9e2 100644 --- a/src/loader/main.ts +++ b/src/loader/main.ts @@ -37,7 +37,7 @@ const createContext = < }, }); -const forceUnloadPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => { +const forceUnloadMainPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => { unregisterStyleMap[id]?.forEach((unregister) => unregister()); delete unregisterStyleMap[id]; @@ -47,7 +47,7 @@ const forceUnloadPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => { console.log('[YTMusic]', `"${id}" plugin is unloaded`); }; -export const forceLoadPlugin = async (id: keyof PluginBuilderList, win: BrowserWindow) => { +export const forceLoadMainPlugin = async (id: keyof PluginBuilderList, win: BrowserWindow) => { const builder = allPluginBuilders[id]; Promise.allSettled( @@ -88,7 +88,7 @@ export const forceLoadPlugin = async (id: keyof PluginBuilderList, win: BrowserW } }; -export const loadAllPlugins = async (win: BrowserWindow) => { +export const loadAllMainPlugins = async (win: BrowserWindow) => { const pluginConfigs = config.plugins.getPlugins(); for (const [pluginId, builder] of Object.entries(allPluginBuilders)) { @@ -97,25 +97,25 @@ export const loadAllPlugins = async (win: BrowserWindow) => { const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {}); if (config.enabled) { - await forceLoadPlugin(pluginId as keyof PluginBuilderList, win); + await forceLoadMainPlugin(pluginId as keyof PluginBuilderList, win); } else { if (loadedPluginMap[pluginId as keyof PluginBuilderList]) { - forceUnloadPlugin(pluginId as keyof PluginBuilderList, win); + forceUnloadMainPlugin(pluginId as keyof PluginBuilderList, win); } } } }; -export const unloadAllPlugins = (win: BrowserWindow) => { +export const unloadAllMainPlugins = (win: BrowserWindow) => { for (const id of Object.keys(loadedPluginMap)) { - forceUnloadPlugin(id as keyof PluginBuilderList, win); + forceUnloadMainPlugin(id as keyof PluginBuilderList, win); } }; -export const getLoadedPlugin = (id: Key): MainPlugin | undefined => { +export const getLoadedMainPlugin = (id: Key): MainPlugin | undefined => { return loadedPluginMap[id]; }; -export const getLoadedAllPlugins = () => { +export const getAllLoadedMainPlugins = () => { return loadedPluginMap; }; export const registerMainPlugin = ( diff --git a/src/loader/renderer.ts b/src/loader/renderer.ts new file mode 100644 index 00000000..5c11e778 --- /dev/null +++ b/src/loader/renderer.ts @@ -0,0 +1,97 @@ +import { deepmerge } from 'deepmerge-ts'; + +import { + PluginBaseConfig, PluginBuilder, + RendererPlugin, + RendererPluginContext, + RendererPluginFactory +} from '../plugins/utils/builder'; + +const allPluginFactoryList: Record> = {}; +const allPluginBuilders: Record> = {}; +const unregisterStyleMap: Record void)[]> = {}; +const loadedPluginMap: Record> = {}; + +const createContext = < + Key extends keyof PluginBuilderList, + Config extends PluginBaseConfig = PluginBuilderList[Key]['config'], +>(id: Key): RendererPluginContext => ({ + getConfig: async () => { + return await window.ipcRenderer.invoke('get-config', id) as Config; + }, + setConfig: async (newConfig) => { + await window.ipcRenderer.invoke('set-config', id, newConfig); + }, + + invoke: async (event: string, ...args: unknown[]): Promise => { + return await window.ipcRenderer.invoke(event, ...args) as Return; + }, + on: (event: string, listener) => { + window.ipcRenderer.on(event, async (_, ...args) => listener(...args as never)); + }, +}); + +const forceUnloadRendererPlugin = (id: keyof PluginBuilderList) => { + unregisterStyleMap[id]?.forEach((unregister) => unregister()); + delete unregisterStyleMap[id]; + + loadedPluginMap[id]?.onUnload?.(); + delete loadedPluginMap[id]; + + console.log('[YTMusic]', `"${id}" plugin is unloaded`); +}; + +export const forceLoadRendererPlugin = async (id: keyof PluginBuilderList) => { + try { + const factory = allPluginFactoryList[id]; + if (!factory) return; + + const context = createContext(id); + const plugin = await factory(context); + loadedPluginMap[id] = plugin; + plugin.onLoad?.(); + + console.log('[YTMusic]', `"${id}" plugin is loaded`); + } catch (err) { + console.log('[YTMusic]', `Cannot initialize "${id}" plugin: ${String(err)}`); + } +}; + +export const loadAllRendererPlugins = async () => { + const pluginConfigs = window.mainConfig.plugins.getPlugins(); + + for (const [pluginId, builder] of Object.entries(allPluginBuilders)) { + const typedBuilder = builder as PluginBuilderList[keyof PluginBuilderList]; + + const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {}); + + if (config.enabled) { + await forceLoadRendererPlugin(pluginId as keyof PluginBuilderList); + } else { + if (loadedPluginMap[pluginId as keyof PluginBuilderList]) { + forceUnloadRendererPlugin(pluginId as keyof PluginBuilderList); + } + } + } +}; + +export const unloadAllRendererPlugins = () => { + for (const id of Object.keys(loadedPluginMap)) { + forceUnloadRendererPlugin(id as keyof PluginBuilderList); + } +}; + +export const getLoadedRendererPlugin = (id: Key): RendererPlugin | undefined => { + return loadedPluginMap[id]; +}; +export const getAllLoadedRendererPlugins = () => { + return loadedPluginMap; +}; +export const registerRendererPlugin = ( + id: string, + builder: PluginBuilder, + factory?: RendererPluginFactory, +) => { + if (factory) allPluginFactoryList[id] = factory; + allPluginBuilders[id] = builder; +}; diff --git a/src/renderer.ts b/src/renderer.ts index 603dd21b..9478d841 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,16 +1,14 @@ // eslint-disable-next-line import/order import { rendererPlugins } from 'virtual:RendererPlugins'; - import { pluginBuilders } from 'virtual:PluginBuilders'; -import { deepmerge } from 'deepmerge-ts'; - -import { PluginBaseConfig, RendererPluginContext, RendererPluginFactory } from './plugins/utils/builder'; +import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugins/utils/builder'; 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'; let api: Element | null = null; @@ -96,73 +94,19 @@ function onApiLoaded() { } } -const createContext = < - Key extends keyof PluginBuilderList, - Config extends PluginBaseConfig = PluginBuilderList[Key]['config'], ->(name: Key): RendererPluginContext => ({ - getConfig: async () => { - const result = await window.ipcRenderer.invoke('get-config', name) as Config; - - return result; - }, - setConfig: async (newConfig) => { - await window.ipcRenderer.invoke('set-config', name, newConfig); - }, - - invoke: async (event: string, ...args: unknown[]): Promise => { - return await window.ipcRenderer.invoke(event, ...args) as Return; - }, - on: (event: string, listener) => { - window.ipcRenderer.on(event, async (_, ...args) => listener(...args as never)); - }, -}); - (async () => { - const pluginConfig = window.mainConfig.plugins.getPlugins(); + Object.entries(pluginBuilders).forEach(([id, builder]) => { + const typedBuilder = builder as PluginBuilder; + const plugin = rendererPlugins[id] as RendererPluginFactory | undefined; - const rendererPluginList = Object.entries(rendererPlugins); - const rendererPluginResult = await Promise.allSettled( - rendererPluginList - .filter(([id]) => { - const typedId = id as keyof PluginBuilderList; - const config = deepmerge(pluginBuilders[typedId].config, pluginConfig[typedId] ?? {}); - - return config.enabled; - }) - .map(async ([id, builder]) => { - const context = createContext(id as keyof PluginBuilderList); - return [id, await (builder as RendererPluginFactory)(context)] as const; - }), - ); - - rendererPluginResult.forEach((it, index) => { - if (it.status === 'rejected') { - const id = rendererPluginList[index][0]; - console.error('[YTMusic]', `Cannot load plugin "${id}"`); - console.trace(it.reason); - } - }); - - const loadedRendererPluginList = rendererPluginResult - .map((it) => it.status === 'fulfilled' ? it.value : null) - .filter(Boolean); - - loadedRendererPluginList.forEach(([id, plugin]) => { - try { - plugin.onLoad?.(); - console.log('[YTMusic]', `"${id}" plugin is loaded`); - } catch (error) { - console.error('[YTMusic]', `Cannot load plugin "${id}"`); - console.trace(error); - } + registerRendererPlugin(id, typedBuilder, plugin); }); + await loadAllRendererPlugins(); window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => { - const plugin = loadedRendererPluginList.find(([pluginId]) => pluginId === id); + const plugin = getAllLoadedRendererPlugins()[id]; - if (plugin) { - plugin[1].onConfigChange?.(newConfig); - } + if (plugin) plugin.onConfigChange?.(newConfig); }); // Inject song-info provider