diff --git a/package.json b/package.json index 0cadd87d..77f26742 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "@cliqz/adblocker-electron": "1.26.11", "@cliqz/adblocker-electron-preload": "1.26.11", "@electron-toolkit/tsconfig": "^1.0.1", + "@electron/remote": "2.1.0", "@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/main": "0.12.0", "@foobar404/wave": "2.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f44fda71..c7443f23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ dependencies: '@electron-toolkit/tsconfig': specifier: ^1.0.1 version: 1.0.1(@types/node@20.8.6) + '@electron/remote': + specifier: 2.1.0 + version: 2.1.0(electron@27.0.4) '@ffmpeg.wasm/core-mt': specifier: 0.12.0 version: 0.12.0 @@ -568,6 +571,14 @@ packages: - supports-color dev: true + /@electron/remote@2.1.0(electron@27.0.4): + resolution: {integrity: sha512-38jzz2beoYTo0DNS+aoaGyLS/fHeNTAc1Aom6HlYsxKnvVWjcg4xriC7J2IUkYSEDHGKX/D7jUst+mH4dHR6QA==} + peerDependencies: + electron: '>= 13.0.0' + dependencies: + electron: 27.0.4 + dev: false + /@electron/universal@1.4.5: resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==} engines: {node: '>=8.6'} diff --git a/src/config/store.ts b/src/config/store.ts index fb3ebf38..b70d9562 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -163,13 +163,7 @@ const migrations = { export default new Store({ defaults: { ...defaults, - plugins: Object.entries(allPlugins).reduce( - (prev, [id, plugin]) => ({ - ...prev, - [id]: plugin.config, - }), - {}, - ), + // README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here }, clearInvalidConfig: false, migrations, diff --git a/src/index.ts b/src/index.ts index 86097cb0..6442b0b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -163,7 +163,7 @@ const initHook = (win: BrowserWindow) => { const mainPlugin = getAllLoadedMainPlugins()[id]; if (mainPlugin) { if (config.enabled && typeof mainPlugin.backend !== 'function') { - mainPlugin.backend?.onConfigChange?.(config); + mainPlugin.backend?.onConfigChange?.bind(mainPlugin.backend)?.(config); } } diff --git a/src/loader/main.ts b/src/loader/main.ts index 53db6fc5..9eb4fe5c 100644 --- a/src/loader/main.ts +++ b/src/loader/main.ts @@ -96,16 +96,18 @@ export const forceLoadMainPlugin = async ( loadedPluginMap[id] = plugin; resolve(); } catch (err) { - console.log( + console.error( '[YTMusic]', - `Cannot initialize "${id}" plugin: ${String(err)}`, + `Cannot initialize "${id}" plugin: `, ); + console.trace(err); reject(err); } }); }; export const loadAllMainPlugins = async (win: BrowserWindow) => { + console.log('[YTMusic]', 'Loading all plugins'); const pluginConfigs = config.plugins.getPlugins(); const queue: Promise[] = []; @@ -118,7 +120,7 @@ export const loadAllMainPlugins = async (win: BrowserWindow) => { } } - await Promise.all(queue); + await Promise.allSettled(queue); }; export const unloadAllMainPlugins = (win: BrowserWindow) => { diff --git a/src/loader/menu.ts b/src/loader/menu.ts index 4230eec9..210107f1 100644 --- a/src/loader/menu.ts +++ b/src/loader/menu.ts @@ -35,7 +35,8 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => { console.log('[YTMusic]', `Successfully loaded '${id}::menu'`); } catch (err) { - console.log('[YTMusic]', `Cannot initialize '${id}::menu': ${String(err)}`); + console.error('[YTMusic]', `Cannot initialize '${id}::menu': `); + console.trace(err); } }; diff --git a/src/loader/preload.ts b/src/loader/preload.ts index 7d3e3778..68297d0e 100644 --- a/src/loader/preload.ts +++ b/src/loader/preload.ts @@ -42,10 +42,11 @@ export const forceLoadPreloadPlugin = (id: string) => { console.log('[YTMusic]', `"${id}" plugin is loaded`); } catch (err) { - console.log( + console.error( '[YTMusic]', - `Cannot initialize "${id}" plugin: ${String(err)}`, + `Cannot initialize "${id}" plugin: `, ); + console.trace(err); } }; diff --git a/src/plugins/captions-selector/index.ts b/src/plugins/captions-selector/index.ts index e21417c6..66ceb832 100644 --- a/src/plugins/captions-selector/index.ts +++ b/src/plugins/captions-selector/index.ts @@ -28,12 +28,11 @@ interface CaptionsSelectorConfig { lastCaptionsCode: string; } -const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML); - export default createPlugin< unknown, unknown, { + captionsSettingsButton: HTMLElement; captionTrackList: LanguageOptions[] | null; api: YoutubePlayer | null; config: CaptionsSelectorConfig | null; @@ -98,6 +97,7 @@ export default createPlugin< }, renderer: { + captionsSettingsButton: ElementFromHtml(CaptionsSettingsButtonHTML), captionTrackList: null, api: null, config: null, @@ -133,7 +133,7 @@ export default createPlugin< captionsButtonClickListener() { if (this.config!.disableCaptions) { setTimeout(() => this.api!.unloadModule('captions'), 100); - captionsSettingsButton.style.display = 'none'; + this.captionsSettingsButton.style.display = 'none'; return; } @@ -148,7 +148,7 @@ export default createPlugin< }); } - captionsSettingsButton.style.display = this.captionTrackList?.length + this.captionsSettingsButton.style.display = this.captionTrackList?.length ? 'inline-block' : 'none'; }, 250); @@ -158,20 +158,20 @@ export default createPlugin< this.setConfig = setConfig; }, stop() { - document.querySelector('.right-controls-buttons')?.removeChild(captionsSettingsButton); + document.querySelector('.right-controls-buttons')?.removeChild(this.captionsSettingsButton); document.querySelector('#movie_player')?.unloadModule('captions'); document.querySelector('video')?.removeEventListener('srcChanged', this.videoChangeListener); - captionsSettingsButton.removeEventListener('click', this.captionsButtonClickListener); + this.captionsSettingsButton.removeEventListener('click', this.captionsButtonClickListener); }, onPlayerApiReady(playerApi) { this.api = playerApi; - document.querySelector('.right-controls-buttons')?.append(captionsSettingsButton); + document.querySelector('.right-controls-buttons')?.append(this.captionsSettingsButton); this.captionTrackList = this.api.getOption('captions', 'tracklist') ?? []; document.querySelector('video')?.addEventListener('srcChanged', this.videoChangeListener); - captionsSettingsButton.addEventListener('click', this.captionsButtonClickListener); + this.captionsSettingsButton.addEventListener('click', this.captionsButtonClickListener); }, onConfigChange(newConfig) { this.config = newConfig; diff --git a/src/plugins/downloader/main/index.ts b/src/plugins/downloader/main/index.ts index e3824738..fe2d4a27 100644 --- a/src/plugins/downloader/main/index.ts +++ b/src/plugins/downloader/main/index.ts @@ -36,7 +36,7 @@ import { cache } from '@/providers/decorators'; import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types'; -import { defaultConfig, type DownloaderPluginConfig } from '../index'; +import type { DownloaderPluginConfig } from '../index'; import type { BackendContext } from '@/types/contexts'; @@ -91,7 +91,7 @@ export const getCookieFromWindow = async (win: BrowserWindow) => { .join(';'); }; -let config: DownloaderPluginConfig = defaultConfig; +let config: DownloaderPluginConfig; export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext) => { win = _win; diff --git a/src/plugins/notifications/main.ts b/src/plugins/notifications/main.ts index 04c4597e..c4b4481b 100644 --- a/src/plugins/notifications/main.ts +++ b/src/plugins/notifications/main.ts @@ -5,12 +5,12 @@ import is from 'electron-is'; import { notificationImage } from './utils'; import interactive from './interactive'; -import { defaultConfig, type NotificationsPluginConfig } from './index'; import registerCallback, { type SongInfo } from '@/providers/song-info'; +import type { NotificationsPluginConfig } from './index'; import type { BackendContext } from '@/types/contexts'; -let config: NotificationsPluginConfig = defaultConfig; +let config: NotificationsPluginConfig; const notify = (info: SongInfo) => { // Send the notification diff --git a/src/renderer.ts b/src/renderer.ts index 259f3230..ba6d6b78 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -78,7 +78,7 @@ function onApiLoaded() { Object.entries(getAllLoadedRendererPlugins()) .forEach(([id, plugin]) => { if (typeof plugin.renderer !== 'function') { - plugin.renderer?.onPlayerApiReady?.(api!, createContext(id)); + plugin.renderer?.onPlayerApiReady?.bind(plugin.renderer)?.(api!, createContext(id)); } }); @@ -135,7 +135,7 @@ function onApiLoaded() { if (api) { const plugin = getLoadedRendererPlugin(id); if (plugin && typeof plugin.renderer !== 'function') { - plugin.renderer?.onPlayerApiReady?.(api, createContext(id)); + plugin.renderer?.onPlayerApiReady?.bind(plugin.renderer)?.(api, createContext(id)); } } }, @@ -146,7 +146,7 @@ function onApiLoaded() { (_event, id: string, newConfig: PluginConfig) => { const plugin = getAllLoadedRendererPlugins()[id]; if (plugin && typeof plugin.renderer !== 'function') { - plugin.renderer?.onConfigChange?.(newConfig); + plugin.renderer?.onConfigChange?.bind(plugin.renderer)?.(newConfig); } }, ); diff --git a/src/utils/index.ts b/src/utils/index.ts index baec8b1c..44107b1b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -42,7 +42,7 @@ export const startPlugin = (id: string, def: Plugin try { const start = performance.now(); - lifecycle(options.context as Config & typeof options.context); + lifecycle.bind(def[options.ctx])(options.context as Config & typeof options.context); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); @@ -62,7 +62,7 @@ export const stopPlugin = (id: string, def: PluginD try { const start = performance.now(); - stop(options.context as Config & typeof options.context); + stop.bind(def[options.ctx])(options.context as Config & typeof options.context); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); diff --git a/src/virtual-module.d.ts b/src/virtual-module.d.ts index 3c7d657c..ffb06766 100644 --- a/src/virtual-module.d.ts +++ b/src/virtual-module.d.ts @@ -4,7 +4,6 @@ declare module 'virtual:plugins' { type Plugin = PluginDef; export const mainPlugins: Record; - export const menuPlugins: Record; export const preloadPlugins: Record; export const rendererPlugins: Record; diff --git a/vite-plugins/plugin-importer.ts b/vite-plugins/plugin-importer.ts index ba9700cc..58aea0f7 100644 --- a/vite-plugins/plugin-importer.ts +++ b/vite-plugins/plugin-importer.ts @@ -7,7 +7,7 @@ const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); export const pluginVirtualModuleGenerator = ( - mode: 'main' | 'preload' | 'renderer' | 'menu', + mode: 'main' | 'preload' | 'renderer', ) => { const project = new Project({ tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), @@ -37,7 +37,7 @@ export const pluginVirtualModuleGenerator = ( // prettier-ignore for (const { name, path } of plugins) { const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); - writer.writeLine(`import ${snakeToCamel(name)}Plugin from "./${relativePath}";`); + writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`); } writer.blankLine(); @@ -45,15 +45,17 @@ export const pluginVirtualModuleGenerator = ( // Context-specific exports writer.writeLine(`export const ${mode}Plugins = {`); for (const { name } of plugins) { - writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`); + const checkMode = mode === 'main' ? 'backend' : mode; + // HACK: To avoid situation like importing renderer plugins in main + writer.writeLine(` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`); } writer.writeLine('};'); writer.blankLine(); - // All plugins export + // All plugins export (stub only) // Omit writer.writeLine('export const allPlugins = {'); for (const { name } of plugins) { - writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`); + writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`); } writer.writeLine('};'); writer.blankLine(); diff --git a/vite-plugins/plugin-loader.ts b/vite-plugins/plugin-loader.ts index 677f3253..26fef631 100644 --- a/vite-plugins/plugin-loader.ts +++ b/vite-plugins/plugin-loader.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'; import { resolve, basename } from 'node:path'; import { createFilter } from 'vite'; -import { Project, ts, ObjectLiteralExpression } from 'ts-morph'; +import { Project, ts, ObjectLiteralExpression, VariableDeclarationKind } from 'ts-morph'; import type { PluginOption } from 'vite'; @@ -78,7 +78,7 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu } }); - const contexts = ['backend', 'preload', 'renderer']; + const contexts = ['backend', 'preload', 'renderer', 'menu']; for (const ctx of contexts) { if (mode === 'none') { const index = propertyNames.indexOf(ctx); @@ -89,6 +89,7 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu } if (ctx === mode) continue; + if (ctx === 'menu' && mode === 'backend') continue; const index = propertyNames.indexOf(ctx); if (index === -1) continue; @@ -96,6 +97,45 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu objExpr.getProperty(propertyNames[index])?.remove(); } + const stubObjExpr = src.addVariableStatement({ + isExported: true, + declarationKind: VariableDeclarationKind.Const, + declarations: [{ + name: 'pluginStub', + initializer: (writer) => writer.write(objExpr!.getText()), + }] + }) + .getDeclarations()[0] + .getInitializer() as ObjectLiteralExpression; + + const stubProperties = stubObjExpr.getProperties(); + const stubPropertyNames = stubProperties.map((prop) => { + switch (prop.getKind()) { + case ts.SyntaxKind.PropertyAssignment: + return prop + .asKindOrThrow(ts.SyntaxKind.PropertyAssignment) + .getName(); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return prop + .asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment) + .getName(); + case ts.SyntaxKind.MethodDeclaration: + return prop + .asKindOrThrow(ts.SyntaxKind.MethodDeclaration) + .getName(); + default: + throw new Error('Not implemented'); + } + }); + + if (mode === 'backend') contexts.pop(); + for (const ctx of contexts) { + const index = stubPropertyNames.indexOf(ctx); + if (index === -1) continue; + + stubObjExpr.getProperty(stubPropertyNames[index])?.remove(); + } + return { code: src.getText(), };