diff --git a/.eslintrc.js b/.eslintrc.js index e1698f08..3013908a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,7 @@ module.exports = { 'import/newline-after-import': 'error', 'import/no-default-export': 'off', 'import/no-duplicates': 'error', + 'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], 'import/order': [ 'error', { diff --git a/electron.vite.config.ts b/electron.vite.config.ts index bdaed311..4d2090f9 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,11 +1,19 @@ import { defineConfig, defineViteConfig } from 'electron-vite'; import builtinModules from 'builtin-modules'; +import viteResolve from 'vite-plugin-resolve'; + +import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-virtual-module-generator'; import type { UserConfig } from 'vite'; export default defineConfig({ main: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { + plugins: [ + viteResolve({ + 'virtual:MainPlugins': pluginVirtualModuleGenerator('back'), + }), + ], publicDir: 'assets', build: { lib: { @@ -38,6 +46,11 @@ export default defineConfig({ }), preload: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { + plugins: [ + viteResolve({ + 'virtual:PreloadPlugins': pluginVirtualModuleGenerator('preload'), + }), + ], build: { lib: { entry: 'src/preload.ts', @@ -69,6 +82,11 @@ export default defineConfig({ }), renderer: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { + plugins: [ + viteResolve({ + 'virtual:RendererPlugins': pluginVirtualModuleGenerator('front'), + }), + ], root: './src/', build: { lib: { diff --git a/package.json b/package.json index 8ec9044f..a80d8472 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "!node_modules/**/*.map", "!node_modules/**/*.ts" ], - "asarUnpack": ["assets"], + "asarUnpack": [ + "assets" + ], "mac": { "identity": null, "target": [ @@ -183,12 +185,14 @@ "eslint": "8.53.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-prettier": "5.0.1", + "glob": "10.3.10", "node-gyp": "10.0.1", "playwright": "1.39.0", "rollup": "4.3.0", "typescript": "5.2.2", "utf-8-validate": "6.0.3", "vite": "4.5.0", + "vite-plugin-resolve": "2.5.1", "ws": "8.14.2", "yarpm": "1.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17b76780..0be26678 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ devDependencies: eslint-plugin-prettier: specifier: 5.0.1 version: 5.0.1(eslint@8.53.0)(prettier@3.0.3) + glob: + specifier: 10.3.10 + version: 10.3.10 node-gyp: specifier: 10.0.1 version: 10.0.1 @@ -180,6 +183,9 @@ devDependencies: vite: specifier: 4.5.0 version: 4.5.0 + vite-plugin-resolve: + specifier: 2.5.1 + version: 2.5.1 ws: specifier: 8.14.2 version: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) @@ -4049,6 +4055,10 @@ packages: type-check: 0.4.0 dev: true + /lib-esm@0.4.1: + resolution: {integrity: sha512-tdSqfyryhnl5k09357x2iWmw3WeU84SaoP/vMGw/nw8z8RPTrfu9sxwRApn6p6GyStuBNyASgwXIV8ctZWlG1A==} + dev: true + /lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} dependencies: @@ -5623,6 +5633,12 @@ packages: dev: true optional: true + /vite-plugin-resolve@2.5.1: + resolution: {integrity: sha512-9dD0Yq5JT1RxHQGZOyhC7e/JlhyhMCftCpQ8TPzQa7KEB/3ERnoCPinH3VJk/0C8qHsA+l41bIcHh5BcHBTmAw==} + dependencies: + lib-esm: 0.4.1 + dev: true + /vite@4.5.0: resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/index.ts b/src/index.ts index 9dd042aa..3c40562b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import electronDebug from 'electron-debug'; import { parse } from 'node-html-parser'; import config from './config'; + import { refreshMenu, setApplicationMenu } from './menu'; import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils'; import { isTesting } from './utils/testing'; @@ -19,31 +20,10 @@ import { setupSongInfo } from './providers/song-info'; import { restart, setupAppControls } from './providers/app-controls'; import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler'; -import adblocker from './plugins/adblocker/back'; -import albumColorTheme from './plugins/album-color-theme/back'; -import ambientMode from './plugins/ambient-mode/back'; -import blurNavigationBar from './plugins/blur-nav-bar/back'; -import captionsSelector from './plugins/captions-selector/back'; -import crossfade from './plugins/crossfade/back'; -import discord from './plugins/discord/back'; -import downloader from './plugins/downloader/back'; -import inAppMenu from './plugins/in-app-menu/back'; -import lastFm from './plugins/last-fm/back'; -import lumiaStream from './plugins/lumiastream/back'; -import lyricsGenius from './plugins/lyrics-genius/back'; -import navigation from './plugins/navigation/back'; -import noGoogleLogin from './plugins/no-google-login/back'; -import notifications from './plugins/notifications/back'; -import pictureInPicture, { setOptions as pipSetOptions } from './plugins/picture-in-picture/back'; -import preciseVolume from './plugins/precise-volume/back'; -import qualityChanger from './plugins/quality-changer/back'; -import shortcuts from './plugins/shortcuts/back'; -import sponsorBlock from './plugins/sponsorblock/back'; -import taskbarMediaControl from './plugins/taskbar-mediacontrol/back'; -import touchbar from './plugins/touchbar/back'; -import tunaObs from './plugins/tuna-obs/back'; -import videoToggle from './plugins/video-toggle/back'; -import visualizer from './plugins/visualizer/back'; +// eslint-disable-next-line import/order +import { pluginList as mainPluginList } from 'virtual:MainPlugins'; + +import { setOptions as pipSetOptions } from './plugins/picture-in-picture/back'; import youtubeMusicCSS from './youtube-music.css'; @@ -103,47 +83,18 @@ function onClosed() { mainWindow = null; } -const mainPlugins = { - 'adblocker': adblocker, - 'album-color-theme': albumColorTheme, - 'ambient-mode': ambientMode, - 'blur-nav-bar': blurNavigationBar, - 'captions-selector': captionsSelector, - 'crossfade': crossfade, - 'discord': discord, - 'downloader': downloader, - 'in-app-menu': inAppMenu, - 'last-fm': lastFm, - 'lumiastream': lumiaStream, - 'lyrics-genius': lyricsGenius, - 'navigation': navigation, - 'no-google-login': noGoogleLogin, - 'notifications': notifications, - 'picture-in-picture': pictureInPicture, - 'precise-volume': preciseVolume, - 'quality-changer': qualityChanger, - 'shortcuts': shortcuts, - 'sponsorblock': sponsorBlock, - 'taskbar-mediacontrol': undefined as typeof taskbarMediaControl | undefined, - 'touchbar': undefined as typeof touchbar | undefined, - 'tuna-obs': tunaObs, - 'video-toggle': videoToggle, - 'visualizer': visualizer, -}; -export const mainPluginNames = Object.keys(mainPlugins); +export const mainPluginNames = Object.keys(mainPluginList); if (is.windows()) { - mainPlugins['taskbar-mediacontrol'] = taskbarMediaControl; - delete mainPlugins['touchbar']; + delete mainPluginList['touchbar']; } else if (is.macOS()) { - mainPlugins['touchbar'] = touchbar; - delete mainPlugins['taskbar-mediacontrol']; + delete mainPluginList['taskbar-mediacontrol']; } else { - delete mainPlugins['touchbar']; - delete mainPlugins['taskbar-mediacontrol']; + delete mainPluginList['touchbar']; + delete mainPluginList['taskbar-mediacontrol']; } -ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins)); +ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPluginList)); async function loadPlugins(win: BrowserWindow) { injectCSS(win.webContents, youtubeMusicCSS); @@ -172,9 +123,9 @@ async function loadPlugins(win: BrowserWindow) { for (const [plugin, options] of config.plugins.getEnabled()) { try { - if (Object.hasOwn(mainPlugins, plugin)) { + if (Object.hasOwn(mainPluginList, plugin)) { console.log('Loaded plugin - ' + plugin); - const handler = mainPlugins[plugin as keyof typeof mainPlugins]; + const handler = mainPluginList[plugin as keyof typeof mainPluginList]; if (handler) { await handler(win, options as never); } @@ -262,7 +213,6 @@ async function createMainWindow() { type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture']; const setPiPOptions = config.plugins.isEnabled('picture-in-picture') - // eslint-disable-next-line @typescript-eslint/no-var-requires ? (key: string, value: unknown) => pipSetOptions({ [key]: value }) : () => {}; diff --git a/src/preload.ts b/src/preload.ts index fd735df7..85b901b4 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -3,7 +3,8 @@ import is from 'electron-is'; import config from './config'; -import adblockerPreload from './plugins/adblocker/preload'; +// eslint-disable-next-line import/order +import { pluginList as preloadPluginList } from 'virtual:PreloadPlugins'; import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic'; @@ -15,15 +16,11 @@ export type PluginMapper = { ) }; -const preloadPlugins: PluginMapper<'preload'> = { - 'adblocker': adblockerPreload, -}; - const enabledPluginNameAndOptions = config.plugins.getEnabled(); enabledPluginNameAndOptions.forEach(async ([plugin, options]) => { - if (Object.hasOwn(preloadPlugins, plugin)) { - const handler = preloadPlugins[plugin]; + if (Object.hasOwn(preloadPluginList, plugin)) { + const handler = preloadPluginList[plugin]; try { await handler?.(); } catch (error) { diff --git a/src/renderer.ts b/src/renderer.ts index 809d33ea..cd4bb8ac 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -2,55 +2,8 @@ import setupSongInfo from './providers/song-info-front'; import { setupSongControls } from './providers/song-controls-front'; import { startingPages } from './providers/extracted-data'; -import albumColorThemeRenderer from './plugins/album-color-theme/front'; -import ambientModeRenderer from './plugins/ambient-mode/front'; -import audioCompressorRenderer from './plugins/audio-compressor/front'; -import bypassAgeRestrictionsRenderer from './plugins/bypass-age-restrictions/front'; -import captionsSelectorRenderer from './plugins/captions-selector/front'; -import compactSidebarRenderer from './plugins/compact-sidebar/front'; -import crossfadeRenderer from './plugins/crossfade/front'; -import disableAutoplayRenderer from './plugins/disable-autoplay/front'; -import downloaderRenderer from './plugins/downloader/front'; -import exponentialVolumeRenderer from './plugins/exponential-volume/front'; -import inAppMenuRenderer from './plugins/in-app-menu/front'; -import lyricsGeniusRenderer from './plugins/lyrics-genius/front'; -import navigationRenderer from './plugins/navigation/front'; -import noGoogleLogin from './plugins/no-google-login/front'; -import pictureInPictureRenderer from './plugins/picture-in-picture/front'; -import playbackSpeedRenderer from './plugins/playback-speed/front'; -import preciseVolumeRenderer from './plugins/precise-volume/front'; -import qualityChangerRenderer from './plugins/quality-changer/front'; -import skipSilencesRenderer from './plugins/skip-silences/front'; -import sponsorblockRenderer from './plugins/sponsorblock/front'; -import videoToggleRenderer from './plugins/video-toggle/front'; -import visualizerRenderer from './plugins/visualizer/front'; - -import type { PluginMapper } from './preload'; - -const rendererPlugins: PluginMapper<'renderer'> = { - 'album-color-theme': albumColorThemeRenderer, - 'ambient-mode': ambientModeRenderer, - 'audio-compressor': audioCompressorRenderer, - 'bypass-age-restrictions': bypassAgeRestrictionsRenderer, - 'captions-selector': captionsSelectorRenderer, - 'compact-sidebar': compactSidebarRenderer, - 'crossfade': crossfadeRenderer, - 'disable-autoplay': disableAutoplayRenderer, - 'downloader': downloaderRenderer, - 'exponential-volume': exponentialVolumeRenderer, - 'in-app-menu': inAppMenuRenderer, - 'lyrics-genius': lyricsGeniusRenderer, - 'navigation': navigationRenderer, - 'no-google-login': noGoogleLogin, - 'picture-in-picture': pictureInPictureRenderer, - 'playback-speed': playbackSpeedRenderer, - 'precise-volume': preciseVolumeRenderer, - 'quality-changer': qualityChangerRenderer, - 'skip-silences': skipSilencesRenderer, - 'sponsorblock': sponsorblockRenderer, - 'video-toggle': videoToggleRenderer, - 'visualizer': visualizerRenderer, -}; +// eslint-disable-next-line import/order +import { pluginList as rendererPluginList } from 'virtual:RendererPlugins'; const enabledPluginNameAndOptions = window.mainConfig.plugins.getEnabled(); @@ -140,8 +93,8 @@ function onApiLoaded() { (() => { enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { - if (Object.hasOwn(rendererPlugins, pluginName)) { - const handler = rendererPlugins[pluginName]; + if (Object.hasOwn(rendererPluginList, pluginName)) { + const handler = rendererPluginList[pluginName]; try { await handler?.(options as never); } catch (error) { diff --git a/src/virtual-module.d.ts b/src/virtual-module.d.ts new file mode 100644 index 00000000..41e7eb48 --- /dev/null +++ b/src/virtual-module.d.ts @@ -0,0 +1,16 @@ +declare module 'virtual:MainPlugins' { + import type { BrowserWindow } from 'electron'; + import type { ConfigType } from './config/dynamic'; + + export const pluginList: Record Promise>; +} + +declare module 'virtual:PreloadPlugins' { + export const pluginList: Record Promise>; +} + +declare module 'virtual:RendererPlugins' { + import type { ConfigType } from './config/dynamic'; + + export const pluginList: Record Promise>; +} diff --git a/vite-plugins/plugin-virtual-module-generator.ts b/vite-plugins/plugin-virtual-module-generator.ts new file mode 100644 index 00000000..3e6b62b4 --- /dev/null +++ b/vite-plugins/plugin-virtual-module-generator.ts @@ -0,0 +1,28 @@ +import { existsSync } from 'node:fs'; +import { basename, relative, resolve } from 'node:path'; + +import { globSync } from 'glob'; + +const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); + +export const pluginVirtualModuleGenerator = (mode: 'back' | 'preload' | 'front') => { + const srcPath = resolve(__dirname, '..', 'src'); + + const plugins = globSync(`${srcPath}/plugins/*`) + .map((path) => ({ name: basename(path), path })) + .filter(({ name, path }) => !name.startsWith('utils') && existsSync(resolve(path, `${mode}.ts`))); + + let result = ''; + + for (const { name, path } of plugins) { + result += `import ${snakeToCamel(name)}Plugin from "./${relative(resolve(srcPath, '..'), path).replace(/\\/g, '/')}/${mode}";\n`; + } + + result += 'export const pluginList = {\n'; + for (const { name } of plugins) { + result += ` "${name}": ${snakeToCamel(name)}Plugin,\n`; + } + result += '};'; + + return result; +};