From 6366dc026e012779ed6ef352bc27ca4314e92920 Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Mon, 6 Nov 2023 17:21:29 +0900 Subject: [PATCH] feat: enable `context-isolation` (#1361) --- .eslintignore | 1 + package.json | 4 +- pnpm-lock.yaml | 7 + rollup.renderer.config.ts | 57 ++++++ src/config/dynamic-renderer.ts | 182 +++++++++++++++++ src/config/dynamic.ts | 102 +++------- src/config/plugins.ts | 9 +- src/index.ts | 15 +- src/plugins/adblocker/config.ts | 1 - src/plugins/album-color-theme/front.ts | 2 +- src/plugins/ambient-mode/front.ts | 22 +-- .../captions-selector/config-renderer.ts | 4 + src/plugins/captions-selector/front.ts | 26 +-- src/plugins/crossfade/config-renderer.ts | 4 + src/plugins/crossfade/front.ts | 20 +- src/plugins/downloader/front.ts | 102 +++++----- src/plugins/in-app-menu/back.ts | 7 +- src/plugins/in-app-menu/front.ts | 35 ++-- src/plugins/in-app-menu/menu/panel.ts | 18 +- src/plugins/lyrics-genius/front.ts | 13 +- src/plugins/navigation/front.ts | 6 +- src/plugins/picture-in-picture/front.ts | 7 +- src/plugins/playback-speed/front.ts | 2 +- src/plugins/precise-volume/front.ts | 15 +- src/plugins/quality-changer/front.ts | 6 +- src/plugins/sponsorblock/front.ts | 7 +- src/plugins/utils-renderer.ts | 8 + src/plugins/utils.ts | 27 +-- src/plugins/video-toggle/front.ts | 7 +- .../visualizer/visualizers/butterchurn.ts | 2 +- src/preload.ts | 187 ++---------------- src/providers/song-controls-front.ts | 6 +- src/providers/song-info-front.ts | 39 ++-- src/renderer.ts | 171 ++++++++++++++++ src/reset.d.ts | 8 + 35 files changed, 655 insertions(+), 474 deletions(-) create mode 100644 rollup.renderer.config.ts create mode 100644 src/config/dynamic-renderer.ts create mode 100644 src/plugins/captions-selector/config-renderer.ts create mode 100644 src/plugins/crossfade/config-renderer.ts create mode 100644 src/plugins/utils-renderer.ts create mode 100644 src/renderer.ts diff --git a/.eslintignore b/.eslintignore index 97bae8d4..c91110d8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ .eslintrc.js rollup.main.config.ts rollup.preload.config.ts +rollup.renderer.config.ts diff --git a/package.json b/package.json index 1512deea..59a90f8f 100644 --- a/package.json +++ b/package.json @@ -89,9 +89,10 @@ "scripts": { "test": "playwright test", "test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test", + "rollup:renderer": "rollup -c rollup.renderer.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", "rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", "rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs", - "build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main", + "build": "yarpm-pnpm run rollup:renderer && yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main", "start": "yarpm-pnpm run build && electron ./dist/index.js", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start", "clean": "del-cli dist && del-cli pack", @@ -132,6 +133,7 @@ "dependencies": { "@cliqz/adblocker-electron": "1.26.11", "@cliqz/adblocker-electron-preload": "1.26.11", + "@fastify/deepmerge": "1.3.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 ec07d8ef..7a580676 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ dependencies: '@cliqz/adblocker-electron-preload': specifier: 1.26.11 version: 1.26.11(electron@27.0.3) + '@fastify/deepmerge': + specifier: 1.3.0 + version: 1.3.0 '@ffmpeg.wasm/core-mt': specifier: 0.12.0 version: 0.12.0 @@ -400,6 +403,10 @@ packages: engines: {node: '>=14'} dev: false + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: false + /@ffmpeg.wasm/core-mt@0.12.0: resolution: {integrity: sha512-M9pjL7JQX4AYl3WI8vGcPGPTz/O7JmhW8ac/fHA3oXTxoRAPwYSY/OsY1N9C0XahIM0+fxa1QSLN9Ekz8sBM/Q==} dev: false diff --git a/rollup.renderer.config.ts b/rollup.renderer.config.ts new file mode 100644 index 00000000..c363a17e --- /dev/null +++ b/rollup.renderer.config.ts @@ -0,0 +1,57 @@ +import { defineConfig } from 'rollup'; +import builtinModules from 'builtin-modules'; +import typescript from '@rollup/plugin-typescript'; +import commonjs from '@rollup/plugin-commonjs'; +import nodeResolvePlugin from '@rollup/plugin-node-resolve'; +import json from '@rollup/plugin-json'; +import terser from '@rollup/plugin-terser'; +import { string } from 'rollup-plugin-string'; +import css from 'rollup-plugin-import-css'; +import wasmPlugin from '@rollup/plugin-wasm'; +import image from '@rollup/plugin-image'; + +export default defineConfig({ + plugins: [ + typescript({ + module: 'ESNext', + }), + nodeResolvePlugin({ + browser: false, + preferBuiltins: true, + }), + commonjs({ + ignoreDynamicRequires: true, + }), + json(), + string({ + include: '**/*.html', + }), + css(), + wasmPlugin({ + maxFileSize: 0, + targetEnv: 'browser', + }), + image({ dom: true }), + terser({ + ecma: 2020, + }), + { + closeBundle() { + if (!process.env.ROLLUP_WATCH) { + setTimeout(() => process.exit(0)); + } + }, + name: 'force-close' + }, + ], + input: './src/renderer.ts', + output: { + format: 'cjs', + name: '[name].js', + dir: './dist', + }, + external: [ + 'electron', + ...builtinModules, + ], +}); diff --git a/src/config/dynamic-renderer.ts b/src/config/dynamic-renderer.ts new file mode 100644 index 00000000..4458a178 --- /dev/null +++ b/src/config/dynamic-renderer.ts @@ -0,0 +1,182 @@ +import defaultConfig from './defaults'; + +import { Entries } from '../utils/type-utils'; + +import type { OneOfDefaultConfigKey, ConfigType, PluginConfigOptions } from './dynamic'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig } = {}; + +export const getActivePlugins + = async () => await window.ipcRenderer.invoke('get-active-plugins') as Promise; + +export const isActive + = async (plugin: string) => plugin in (await window.ipcRenderer.invoke('get-active-plugins')); + +/** + * This class is used to create a dynamic synced config for plugins. + * + * @param {string} name - The name of the plugin. + * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false. + * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store. + * + * @example + * const { PluginConfig } = require("../../config/dynamic"); + * const config = new PluginConfig("plugin-name", { enableFront: true }); + * module.exports = { ...config }; + * + * // or + * + * module.exports = (win, options) => { + * const config = new PluginConfig("plugin-name", { + * enableFront: true, + * initialOptions: options, + * }); + * setupMyPlugin(win, config); + * }; + */ +type ValueOf = T[keyof T]; + +export class PluginConfig { + private readonly name: string; + private readonly config: ConfigType; + private readonly defaultConfig: ConfigType; + private readonly enableFront: boolean; + + private subscribers: { [key in keyof ConfigType]?: (config: ConfigType) => void } = {}; + private allSubscribers: ((config: ConfigType) => void)[] = []; + + constructor( + name: T, + options: PluginConfigOptions = { + enableFront: false, + }, + ) { + const pluginDefaultConfig = defaultConfig.plugins[name] ?? {}; + const pluginConfig = options.initialOptions || window.mainConfig.plugins.getOptions(name) || {}; + + this.name = name; + this.enableFront = options.enableFront; + this.defaultConfig = pluginDefaultConfig; + this.config = { ...pluginDefaultConfig, ...pluginConfig }; + + if (this.enableFront) { + this.setupFront(); + } + + activePlugins[name] = this; + } + + get = keyof ConfigType>(key: Key): ConfigType[Key] { + return this.config?.[key]; + } + + set(key: keyof ConfigType, value: ValueOf>) { + this.config[key] = value; + this.onChange(key); + this.save(); + } + + getAll(): ConfigType { + return { ...this.config }; + } + + setAll(options: Partial>) { + if (!options || typeof options !== 'object') { + throw new Error('Options must be an object.'); + } + + let changed = false; + for (const [key, value] of Object.entries(options) as Entries) { + if (this.config[key] !== value) { + if (value !== undefined) this.config[key] = value; + this.onChange(key, false); + changed = true; + } + } + + if (changed) { + for (const fn of this.allSubscribers) { + fn(this.config); + } + } + + this.save(); + } + + getDefaultConfig() { + return this.defaultConfig; + } + + /** + * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true` + * + * Used for options that require a restart to take effect. + */ + setAndMaybeRestart(key: keyof ConfigType, value: ValueOf>) { + this.config[key] = value; + window.mainConfig.plugins.setMenuOptions(this.name, this.config); + this.onChange(key); + } + + subscribe(valueName: keyof ConfigType, fn: (config: ConfigType) => void) { + this.subscribers[valueName] = fn; + } + + subscribeAll(fn: (config: ConfigType) => void) { + this.allSubscribers.push(fn); + } + + /** Called only from back */ + private save() { + window.mainConfig.plugins.setOptions(this.name, this.config); + } + + private onChange(valueName: keyof ConfigType, single: boolean = true) { + this.subscribers[valueName]?.(this.config[valueName] as ConfigType); + if (single) { + for (const fn of this.allSubscribers) { + fn(this.config); + } + } + } + + private setupFront() { + const ignoredMethods = ['subscribe', 'subscribeAll']; + + for (const [fnName, fn] of Object.entries(this) as Entries) { + if (typeof fn !== 'function' || fn.name in ignoredMethods) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return + this[fnName] = (async (...args: any) => await window.ipcRenderer.invoke( + `${this.name}-config-${String(fnName)}`, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ...args, + )) as typeof this[keyof this]; + + this.subscribe = (valueName, fn: (config: ConfigType) => void) => { + if (valueName in this.subscribers) { + console.error(`Already subscribed to ${String(valueName)}`); + } + + this.subscribers[valueName] = fn; + window.ipcRenderer.on( + `${this.name}-config-changed-${String(valueName)}`, + (_, value: ConfigType) => { + fn(value); + }, + ); + window.ipcRenderer.send(`${this.name}-config-subscribe`, valueName); + }; + + this.subscribeAll = (fn: (config: ConfigType) => void) => { + window.ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType) => { + fn(value); + }); + window.ipcRenderer.send(`${this.name}-config-subscribe-all`); + }; + } + } +} diff --git a/src/config/dynamic.ts b/src/config/dynamic.ts index e9381bca..12d34f19 100644 --- a/src/config/dynamic.ts +++ b/src/config/dynamic.ts @@ -1,12 +1,9 @@ -/* eslint-disable @typescript-eslint/require-await */ - -import { ipcMain, ipcRenderer } from 'electron'; +import { ipcMain } from 'electron'; import defaultConfig from './defaults'; import { getOptions, setMenuOptions, setOptions } from './plugins'; - import { sendToFront } from '../providers/app-controls'; import { Entries } from '../utils/type-utils'; @@ -17,30 +14,15 @@ export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfig // eslint-disable-next-line @typescript-eslint/no-explicit-any const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig } = {}; -/** - * [!IMPORTANT!] - * The method is **sync** in the main process and **async** in the renderer process. - */ -export const getActivePlugins - = process.type === 'renderer' - ? async () => ipcRenderer.invoke('get-active-plugins') - : () => activePlugins; +export const getActivePlugins = () => activePlugins; if (process.type === 'browser') { ipcMain.handle('get-active-plugins', getActivePlugins); } -/** - * [!IMPORTANT!] - * The method is **sync** in the main process and **async** in the renderer process. - */ -export const isActive - = process.type === 'renderer' - ? async (plugin: string) => - plugin in (await ipcRenderer.invoke('get-active-plugins')) - : (plugin: string): boolean => plugin in activePlugins; +export const isActive = (plugin: string): boolean => plugin in activePlugins; -interface PluginConfigOptions { +export interface PluginConfigOptions { enableFront: boolean; initialOptions?: OneOfDefaultConfig; } @@ -48,9 +30,6 @@ interface PluginConfigOptions { /** * This class is used to create a dynamic synced config for plugins. * - * [!IMPORTANT!] - * The methods are **sync** in the main process and **async** in the renderer process. - * * @param {string} name - The name of the plugin. * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false. * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store. @@ -72,7 +51,7 @@ interface PluginConfigOptions { */ export type ConfigType = typeof defaultConfig.plugins[T]; type ValueOf = T[keyof T]; -type Mode = Mode extends 'r' ? Promise : T; + export class PluginConfig { private readonly name: string; private readonly config: ConfigType; @@ -180,62 +159,25 @@ export class PluginConfig { private setupFront() { const ignoredMethods = ['subscribe', 'subscribeAll']; - if (process.type === 'renderer') { - for (const [fnName, fn] of Object.entries(this) as Entries) { - if (typeof fn !== 'function' || fn.name in ignoredMethods) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return - this[fnName] = (async (...args: any) => await ipcRenderer.invoke( - `${this.name}-config-${String(fnName)}`, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ...args, - )) as typeof this[keyof this]; - - this.subscribe = (valueName, fn: (config: ConfigType) => void) => { - if (valueName in this.subscribers) { - console.error(`Already subscribed to ${String(valueName)}`); - } - - this.subscribers[valueName] = fn; - ipcRenderer.on( - `${this.name}-config-changed-${String(valueName)}`, - (_, value: ConfigType) => { - fn(value); - }, - ); - ipcRenderer.send(`${this.name}-config-subscribe`, valueName); - }; - - this.subscribeAll = (fn: (config: ConfigType) => void) => { - ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType) => { - fn(value); - }); - ipcRenderer.send(`${this.name}-config-subscribe-all`); - }; - } - } else if (process.type === 'browser') { - for (const [fnName, fn] of Object.entries(this) as Entries) { - if (typeof fn !== 'function' || fn.name in ignoredMethods) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return - ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args)); + for (const [fnName, fn] of Object.entries(this) as Entries) { + if (typeof fn !== 'function' || fn.name in ignoredMethods) { + return; } - ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType) => { - this.subscribe(valueName, (value) => { - sendToFront(`${this.name}-config-changed-${String(valueName)}`, value); - }); - }); - - ipcMain.on(`${this.name}-config-subscribe-all`, () => { - this.subscribeAll((value) => { - sendToFront(`${this.name}-config-changed`, value); - }); - }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return + ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args)); } + + ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType) => { + this.subscribe(valueName, (value) => { + sendToFront(`${this.name}-config-changed-${String(valueName)}`, value); + }); + }); + + ipcMain.on(`${this.name}-config-subscribe-all`, () => { + this.subscribeAll((value) => { + sendToFront(`${this.name}-config-changed`, value); + }); + }); } } diff --git a/src/config/plugins.ts b/src/config/plugins.ts index c2357f6f..3092b1d2 100644 --- a/src/config/plugins.ts +++ b/src/config/plugins.ts @@ -1,3 +1,5 @@ +import { deepmerge } from '@fastify/deepmerge'; + import store from './store'; import defaultConfig from './defaults'; @@ -9,11 +11,12 @@ interface Plugin { } type DefaultPluginsConfig = typeof defaultConfig.plugins; +const deepmergeFn = deepmerge(); export function getEnabled() { - const plugins = store.get('plugins') as DefaultPluginsConfig; - return (Object.entries(plugins) as Entries).filter(([plugin]) => - isEnabled(plugin), + const plugins = deepmergeFn(defaultConfig.plugins, (store.get('plugins') as DefaultPluginsConfig)); + return (Object.entries(plugins) as Entries).filter(([, options]) => + (options as Plugin).enabled, ); } diff --git a/src/index.ts b/src/index.ts index f5eaaaed..ab88b559 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import path from 'node:path'; +import url from 'node:url'; +import fs from 'node:fs'; import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron'; import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request'; @@ -195,11 +197,8 @@ async function createMainWindow() { backgroundColor: '#000', show: false, webPreferences: { - // TODO: re-enable contextIsolation once it can work with FFMpeg.wasm - // Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126 - contextIsolation: false, + contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - nodeIntegrationInSubFrames: true, ...(isTesting() ? undefined : { @@ -335,6 +334,14 @@ async function createMainWindow() { removeContentSecurityPolicy(); + win.webContents.on('dom-ready', () => { + const rendererScriptPath = path.join(__dirname, 'renderer.js'); + win.webContents.executeJavaScriptInIsolatedWorld(0, [{ + code: fs.readFileSync(rendererScriptPath, 'utf-8') + ';0', + url: url.pathToFileURL(rendererScriptPath).toString(), + }], true); + }); + win.webContents.loadURL(urlToLoad); return win; diff --git a/src/plugins/adblocker/config.ts b/src/plugins/adblocker/config.ts index d646b0e2..0ff54eb3 100644 --- a/src/plugins/adblocker/config.ts +++ b/src/plugins/adblocker/config.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/await-thenable */ /* renderer */ import { blockers } from './blocker-types'; diff --git a/src/plugins/album-color-theme/front.ts b/src/plugins/album-color-theme/front.ts index a068bf47..a07d08fd 100644 --- a/src/plugins/album-color-theme/front.ts +++ b/src/plugins/album-color-theme/front.ts @@ -1,6 +1,6 @@ import { FastAverageColor } from 'fast-average-color'; -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; function hexToHSL(H: string) { // Convert hex to RGB first diff --git a/src/plugins/ambient-mode/front.ts b/src/plugins/ambient-mode/front.ts index 1fae3640..227686fe 100644 --- a/src/plugins/ambient-mode/front.ts +++ b/src/plugins/ambient-mode/front.ts @@ -1,6 +1,4 @@ -import { ipcRenderer } from 'electron'; - -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; export default (config: ConfigType<'ambient-mode'>) => { let interpolationTime = config.interpolationTime; // interpolation time (ms) @@ -30,7 +28,7 @@ export default (config: ConfigType<'ambient-mode'>) => { /* effect */ let lastEffectWorkId: number | null = null; let lastImageData: ImageData | null = null; - + const onSync = () => { if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId); @@ -40,6 +38,7 @@ export default (config: ConfigType<'ambient-mode'>) => { const width = qualityRatio; let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1); if (!Number.isFinite(height)) height = width; + if (!height) return; context.globalAlpha = 1; if (lastImageData) { @@ -50,8 +49,7 @@ export default (config: ConfigType<'ambient-mode'>) => { } context.drawImage(video, 0, 0, width, height); - const nowImageData = context.getImageData(0, 0, width, height); - lastImageData = nowImageData; + lastImageData = context.getImageData(0, 0, width, height); // current image data lastEffectWorkId = null; }); @@ -102,8 +100,8 @@ export default (config: ConfigType<'ambient-mode'>) => { applyVideoAttributes(); }; - ipcRenderer.on('ambient-mode:config-change', onConfigSync); - + window.ipcRenderer.on('ambient-mode:config-change', onConfigSync); + /* hooking */ let canvasInterval: NodeJS.Timeout | null = null; canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); @@ -135,17 +133,17 @@ export default (config: ConfigType<'ambient-mode'>) => { observer.disconnect(); resizeObserver.disconnect(); - ipcRenderer.off('ambient-mode:config-change', onConfigSync); + window.ipcRenderer.removeListener('ambient-mode:config-change', onConfigSync); window.removeEventListener('resize', applyVideoAttributes); wrapper.removeChild(blurCanvas); }; }; - + const playerPage = document.querySelector('#player-page'); const ytmusicAppLayout = document.querySelector('#layout'); - + const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes') { @@ -164,4 +162,4 @@ export default (config: ConfigType<'ambient-mode'>) => { if (playerPage) { observer.observe(playerPage, { attributes: true }); } -}; \ No newline at end of file +}; diff --git a/src/plugins/captions-selector/config-renderer.ts b/src/plugins/captions-selector/config-renderer.ts new file mode 100644 index 00000000..867ab9dc --- /dev/null +++ b/src/plugins/captions-selector/config-renderer.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic-renderer'; + +const configRenderer = new PluginConfig('captions-selector', { enableFront: true }); +export default configRenderer; diff --git a/src/plugins/captions-selector/front.ts b/src/plugins/captions-selector/front.ts index 113e3ef8..95d973ba 100644 --- a/src/plugins/captions-selector/front.ts +++ b/src/plugins/captions-selector/front.ts @@ -1,13 +1,8 @@ -/* eslint-disable @typescript-eslint/await-thenable */ -/* renderer */ - -import { ipcRenderer } from 'electron'; - -import configProvider from './config'; +import configProvider from './config-renderer'; import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { YoutubePlayer } from '../../types/youtube-player'; import type { ConfigType } from '../../config/dynamic'; @@ -25,18 +20,17 @@ interface LanguageOptions { vss_id: string; } -let config: ConfigType<'captions-selector'>; +let captionsSelectorConfig: ConfigType<'captions-selector'>; const $ = (selector: string): Element => document.querySelector(selector)!; const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML); -export default async () => { - // RENDERER - config = await configProvider.getAll(); +export default () => { + captionsSelectorConfig = configProvider.getAll(); configProvider.subscribeAll((newConfig) => { - config = newConfig; + captionsSelectorConfig = newConfig; }); document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true }); }; @@ -47,7 +41,7 @@ function setup(api: YoutubePlayer) { let captionTrackList = api.getOption('captions', 'tracklist') ?? []; $('video').addEventListener('srcChanged', () => { - if (config.disableCaptions) { + if (captionsSelectorConfig.disableCaptions) { setTimeout(() => api.unloadModule('captions'), 100); captionsSettingsButton.style.display = 'none'; return; @@ -58,9 +52,9 @@ function setup(api: YoutubePlayer) { setTimeout(() => { captionTrackList = api.getOption('captions', 'tracklist') ?? []; - if (config.autoload && config.lastCaptionsCode) { + if (captionsSelectorConfig.autoload && captionsSelectorConfig.lastCaptionsCode) { api.setOption('captions', 'track', { - languageCode: config.lastCaptionsCode, + languageCode: captionsSelectorConfig.lastCaptionsCode, }); } @@ -82,7 +76,7 @@ function setup(api: YoutubePlayer) { 'None', ]; - currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number; + currentIndex = await window.ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number; if (currentIndex === null) { return; } diff --git a/src/plugins/crossfade/config-renderer.ts b/src/plugins/crossfade/config-renderer.ts new file mode 100644 index 00000000..d9c1af27 --- /dev/null +++ b/src/plugins/crossfade/config-renderer.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic-renderer'; + +const config = new PluginConfig('crossfade', { enableFront: true }); +export default config; diff --git a/src/plugins/crossfade/front.ts b/src/plugins/crossfade/front.ts index a38cd713..cc96d4e6 100644 --- a/src/plugins/crossfade/front.ts +++ b/src/plugins/crossfade/front.ts @@ -1,13 +1,9 @@ -/* eslint-disable @typescript-eslint/await-thenable */ -/* renderer */ - -import { ipcRenderer } from 'electron'; import { Howl } from 'howler'; // Extracted from https://github.com/bitfasching/VolumeFader import { VolumeFader } from './fader'; -import configProvider from './config'; +import configProvider from './config-renderer'; import defaultConfigs from '../../config/defaults'; @@ -19,11 +15,11 @@ let waitForTransition: Promise; const defaultConfig = defaultConfigs.plugins.crossfade; -let config: ConfigType<'crossfade'>; +let crossfadeConfig: ConfigType<'crossfade'>; -const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number); +const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(crossfadeConfig[key]) || (defaultConfig[key] as number); -const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise; +const getStreamURL = async (videoID: string) => window.ipcRenderer.invoke('audio-url', videoID) as Promise; const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); @@ -119,7 +115,7 @@ const onApiLoaded = () => { return; } - await createAudioForCrossfade(url); + createAudioForCrossfade(url); }); }; @@ -150,11 +146,11 @@ const crossfade = (cb: () => void) => { }); }; -export default async () => { - config = await configProvider.getAll(); +export default () => { + crossfadeConfig = configProvider.getAll(); configProvider.subscribeAll((newConfig) => { - config = newConfig; + crossfadeConfig = newConfig; }); document.addEventListener('apiLoaded', onApiLoaded, { diff --git a/src/plugins/downloader/front.ts b/src/plugins/downloader/front.ts index 06d6cfbb..ef29862a 100644 --- a/src/plugins/downloader/front.ts +++ b/src/plugins/downloader/front.ts @@ -1,10 +1,8 @@ -import { ipcRenderer } from 'electron'; - import downloadHTML from './templates/download.html'; import defaultConfig from '../../config/defaults'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { getSongInfo } from '../../providers/song-info-front'; let menu: Element | null = null; @@ -13,55 +11,55 @@ const downloadButton = ElementFromHtml(downloadHTML); let doneFirstLoad = false; -const menuObserver = new MutationObserver(() => { - if (!menu) { - menu = getSongMenu(); - if (!menu) { - return; - } - } - - if (menu.contains(downloadButton)) { - return; - } - - const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; - if (!menuUrl?.includes('watch?') && doneFirstLoad) { - return; - } - - menu.prepend(downloadButton); - progress = document.querySelector('#ytmcustom-download'); - - if (doneFirstLoad) { - return; - } - - setTimeout(() => doneFirstLoad ||= true, 500); -}); - -window.download = () => { - let videoUrl = getSongMenu() - // Selector of first button which is always "Start Radio" - ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint') - ?.getAttribute('href'); - if (videoUrl) { - if (videoUrl.startsWith('watch?')) { - videoUrl = defaultConfig.url + '/' + videoUrl; - } - - if (videoUrl.includes('?playlist=')) { - ipcRenderer.send('download-playlist-request', videoUrl); - return; - } - } else { - videoUrl = getSongInfo().url || window.location.href; - } - - ipcRenderer.send('download-song', videoUrl); -}; - export default () => { + const menuObserver = new MutationObserver(() => { + if (!menu) { + menu = getSongMenu(); + if (!menu) { + return; + } + } + + if (menu.contains(downloadButton)) { + return; + } + + const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href; + if (!menuUrl?.includes('watch?') && doneFirstLoad) { + return; + } + + menu.prepend(downloadButton); + progress = document.querySelector('#ytmcustom-download'); + + if (doneFirstLoad) { + return; + } + + setTimeout(() => doneFirstLoad ||= true, 500); + }); + + window.download = () => { + let videoUrl = getSongMenu() + // Selector of first button which is always "Start Radio" + ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint') + ?.getAttribute('href'); + if (videoUrl) { + if (videoUrl.startsWith('watch?')) { + videoUrl = defaultConfig.url + '/' + videoUrl; + } + + if (videoUrl.includes('?playlist=')) { + window.ipcRenderer.send('download-playlist-request', videoUrl); + return; + } + } else { + videoUrl = getSongInfo().url || window.location.href; + } + + window.ipcRenderer.send('download-song', videoUrl); + }; + document.addEventListener('apiLoaded', () => { menuObserver.observe(document.querySelector('ytmusic-popup-container')!, { childList: true, @@ -69,7 +67,7 @@ export default () => { }); }, { once: true, passive: true }); - ipcRenderer.on('downloader-feedback', (_, feedback: string) => { + window.ipcRenderer.on('downloader-feedback', (_, feedback: string) => { if (progress) { progress.innerHTML = feedback || 'Download'; } else { diff --git a/src/plugins/in-app-menu/back.ts b/src/plugins/in-app-menu/back.ts index 673ced93..9f5f3f05 100644 --- a/src/plugins/in-app-menu/back.ts +++ b/src/plugins/in-app-menu/back.ts @@ -1,6 +1,6 @@ import { register } from 'electron-localshortcut'; -import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron'; +import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron'; import titlebarStyle from './titlebar.css'; @@ -64,4 +64,9 @@ export default (win: BrowserWindow) => { win.on('maximize', () => win.webContents.send('window-maximize')); ipcMain.handle('window-unmaximize', () => win.unmaximize()); win.on('unmaximize', () => win.webContents.send('window-unmaximize')); + + ipcMain.handle('image-path-to-data-url', (_, imagePath: string) => { + const nativeImageIcon = nativeImage.createFromPath(imagePath); + return nativeImageIcon?.toDataURL(); + }); }; diff --git a/src/plugins/in-app-menu/front.ts b/src/plugins/in-app-menu/front.ts index d7f96793..885ff409 100644 --- a/src/plugins/in-app-menu/front.ts +++ b/src/plugins/in-app-menu/front.ts @@ -1,5 +1,3 @@ -import { ipcRenderer, Menu } from 'electron'; - import { createPanel } from './menu/panel'; import logo from './assets/menu.svg'; @@ -8,8 +6,7 @@ import minimize from './assets/minimize.svg'; import maximize from './assets/maximize.svg'; import unmaximize from './assets/unmaximize.svg'; -import { isEnabled } from '../../config/plugins'; -import config from '../../config'; +import type { Menu } from 'electron'; function $(selector: string) { return document.querySelector(selector); @@ -19,8 +16,8 @@ const isMacOS = navigator.userAgent.includes('Macintosh'); const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; export default async () => { - const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls'); - let hideMenu = config.get('options.hideMenu'); + const hideDOMWindowControls = window.mainConfig.get('plugins.in-app-menu.hideDOMWindowControls'); + let hideMenu = window.mainConfig.get('options.hideMenu'); const titleBar = document.createElement('title-bar'); const navBar = document.querySelector('#nav-bar-background'); let maximizeButton: HTMLButtonElement; @@ -42,7 +39,7 @@ export default async () => { }; logo.onclick = logoClick; - ipcRenderer.on('toggleMenu', logoClick); + window.ipcRenderer.on('toggleMenu', logoClick); if (!isMacOS) titleBar.appendChild(logo); document.body.appendChild(titleBar); @@ -55,10 +52,10 @@ export default async () => { const minimizeButton = document.createElement('button'); minimizeButton.classList.add('window-control'); minimizeButton.appendChild(minimize); - minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize'); + minimizeButton.onclick = () => window.ipcRenderer.invoke('window-minimize'); maximizeButton = document.createElement('button'); - if (await ipcRenderer.invoke('window-is-maximized')) { + if (await window.ipcRenderer.invoke('window-is-maximized')) { maximizeButton.classList.add('window-control'); maximizeButton.appendChild(unmaximize); } else { @@ -66,27 +63,27 @@ export default async () => { maximizeButton.appendChild(maximize); } maximizeButton.onclick = async () => { - if (await ipcRenderer.invoke('window-is-maximized')) { + if (await window.ipcRenderer.invoke('window-is-maximized')) { // change icon to maximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(maximize); // call unmaximize - await ipcRenderer.invoke('window-unmaximize'); + await window.ipcRenderer.invoke('window-unmaximize'); } else { // change icon to unmaximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(unmaximize); // call maximize - await ipcRenderer.invoke('window-maximize'); + await window.ipcRenderer.invoke('window-maximize'); } }; const closeButton = document.createElement('button'); closeButton.classList.add('window-control'); closeButton.appendChild(close); - closeButton.onclick = () => ipcRenderer.invoke('window-close'); + closeButton.onclick = () => window.ipcRenderer.invoke('window-close'); // Create a container div for the window control buttons const windowControlsContainer = document.createElement('div'); @@ -118,7 +115,7 @@ export default async () => { if (child !== logo) child.remove(); }); - const menu = await ipcRenderer.invoke('get-menu') as Menu | null; + const menu = await window.ipcRenderer.invoke('get-menu') as Menu | null; if (!menu) return; menu.items.forEach((menuItem) => { @@ -137,22 +134,22 @@ export default async () => { document.title = 'Youtube Music'; - ipcRenderer.on('refreshMenu', () => updateMenu()); - ipcRenderer.on('window-maximize', () => { + window.ipcRenderer.on('refreshMenu', () => updateMenu()); + window.ipcRenderer.on('window-maximize', () => { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.appendChild(unmaximize); } }); - ipcRenderer.on('window-unmaximize', () => { + window.ipcRenderer.on('window-unmaximize', () => { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.appendChild(unmaximize); } }); - if (isEnabled('picture-in-picture')) { - ipcRenderer.on('pip-toggle', () => { + if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { + window.ipcRenderer.on('pip-toggle', () => { updateMenu(); }); } diff --git a/src/plugins/in-app-menu/menu/panel.ts b/src/plugins/in-app-menu/menu/panel.ts index c15057b3..18bd39f7 100644 --- a/src/plugins/in-app-menu/menu/panel.ts +++ b/src/plugins/in-app-menu/menu/panel.ts @@ -1,8 +1,8 @@ -import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron'; - import Icons from './icons'; -import { ElementFromHtml } from '../../utils'; +import { ElementFromHtml } from '../../utils-renderer'; + +import type { MenuItem } from 'electron'; interface PanelOptions { placement?: 'bottom' | 'right'; @@ -19,7 +19,7 @@ export const createPanel = ( const panel = document.createElement('menu-panel'); panel.style.zIndex = `${options.order}`; - const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => { + const updateIconState = async (iconWrapper: HTMLElement, item: MenuItem) => { if (item.type === 'checkbox') { if (item.checked) iconWrapper.innerHTML = Icons.checkbox; else iconWrapper.innerHTML = ''; @@ -27,8 +27,8 @@ export const createPanel = ( if (item.checked) iconWrapper.innerHTML = Icons.radio.checked; else iconWrapper.innerHTML = Icons.radio.unchecked; } else { - const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon; - const iconURL = nativeImageIcon?.toDataURL(); + const iconURL = typeof item.icon === 'string' ? + await window.ipcRenderer.invoke('image-path-to-data-url') as string : item.icon?.toDataURL(); if (iconURL) iconWrapper.style.background = `url(${iconURL})`; } @@ -46,8 +46,8 @@ export const createPanel = ( menu.append(item.label); menu.addEventListener('click', async () => { - await ipcRenderer.invoke('menu-event', item.commandId); - const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + await window.ipcRenderer.invoke('menu-event', item.commandId); + const menuItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; if (menuItem) { updateIconState(iconWrapper, menuItem); @@ -56,7 +56,7 @@ export const createPanel = ( await Promise.all( radioGroups.map(async ([item, iconWrapper]) => { if (item.commandId === menuItem.commandId) return; - const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + const newItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; if (newItem) updateIconState(iconWrapper, newItem); }) diff --git a/src/plugins/lyrics-genius/front.ts b/src/plugins/lyrics-genius/front.ts index ebfc0071..c86b0ddd 100644 --- a/src/plugins/lyrics-genius/front.ts +++ b/src/plugins/lyrics-genius/front.ts @@ -1,6 +1,3 @@ -import { ipcRenderer } from 'electron'; -import is from 'electron-is'; - import type { SongInfo } from '../../providers/song-info'; export default () => { @@ -22,9 +19,9 @@ export default () => { } }; - let unregister: (() => void) | null = null; + let unregister: (() => void) | null = null; - ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { + window.ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { unregister?.(); setTimeout(async () => { @@ -38,7 +35,7 @@ export default () => { // Check if disabled if (!tabs.lyrics?.hasAttribute('disabled')) return; - const lyrics = await ipcRenderer.invoke( + const lyrics = await window.ipcRenderer.invoke( 'search-genius-lyrics', extractedSongInfo, ) as string | null; @@ -50,7 +47,7 @@ export default () => { return; } - if (is.dev()) { + if (window.electronIs.dev()) { console.log('Fetched lyrics from Genius'); } @@ -58,7 +55,7 @@ export default () => { const lyricsContainer = document.querySelector( '[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer', ); - + if (lyricsContainer) { callback?.(); diff --git a/src/plugins/navigation/front.ts b/src/plugins/navigation/front.ts index 61972b9f..cc03a98b 100644 --- a/src/plugins/navigation/front.ts +++ b/src/plugins/navigation/front.ts @@ -1,12 +1,10 @@ -import { ipcRenderer } from 'electron'; - import forwardHTML from './templates/forward.html'; import backHTML from './templates/back.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; export function run() { - ipcRenderer.on('navigation-css-ready', () => { + window.ipcRenderer.on('navigation-css-ready', () => { const forwardButton = ElementFromHtml(forwardHTML); const backButton = ElementFromHtml(backHTML); const menu = document.querySelector('#right-content'); diff --git a/src/plugins/picture-in-picture/front.ts b/src/plugins/picture-in-picture/front.ts index 28dd2df0..406137a3 100644 --- a/src/plugins/picture-in-picture/front.ts +++ b/src/plugins/picture-in-picture/front.ts @@ -1,4 +1,3 @@ -import { ipcRenderer } from 'electron'; import { toKeyEvent } from 'keyboardevent-from-electron-accelerator'; import keyEventAreEqual from 'keyboardevents-areequal'; @@ -6,7 +5,7 @@ import pipHTML from './templates/picture-in-picture.html'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import type { ConfigType } from '../../config/dynamic'; @@ -85,7 +84,7 @@ const togglePictureInPicture = async () => { } } - ipcRenderer.send('picture-in-picture'); + window.ipcRenderer.send('picture-in-picture'); return false; }; // For UI (HTML) @@ -105,7 +104,7 @@ const listenForToggle = () => { const titlebar = $('.cet-titlebar'); - ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { + window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { if (originalExitButton && player) { if (isPip) { replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture()); diff --git a/src/plugins/playback-speed/front.ts b/src/plugins/playback-speed/front.ts index 5c491a03..b8a0864e 100644 --- a/src/plugins/playback-speed/front.ts +++ b/src/plugins/playback-speed/front.ts @@ -1,7 +1,7 @@ import sliderHTML from './templates/slider.html'; import { getSongMenu } from '../../providers/dom-elements'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { singleton } from '../../providers/decorators'; diff --git a/src/plugins/precise-volume/front.ts b/src/plugins/precise-volume/front.ts index e5bd1493..d4c9bfb4 100644 --- a/src/plugins/precise-volume/front.ts +++ b/src/plugins/precise-volume/front.ts @@ -1,6 +1,3 @@ -import { ipcRenderer } from 'electron'; - -import { setOptions, setMenuOptions, isEnabled } from '../../config/plugins'; import { debounce } from '../../providers/decorators'; import { YoutubePlayer } from '../../types/youtube-player'; @@ -18,15 +15,15 @@ export default (_options: ConfigType<'precise-volume'>) => { options = _options; document.addEventListener('apiLoaded', (e) => { api = e.detail; - ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); - ipcRenderer.on('setVolume', (_, value: number) => setVolume(value)); + window.ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); + window.ipcRenderer.on('setVolume', (_, value: number) => setVolume(value)); firstRun(); }, { once: true, passive: true }); }; // Without this function it would rewrite config 20 time when volume change by 20 const writeOptions = debounce(() => { - setOptions('precise-volume', options); + window.mainConfig.plugins.setOptions('precise-volume', options); }, 1000); export const moveVolumeHud = debounce((showVideo: boolean) => { @@ -68,7 +65,7 @@ function firstRun() { injectVolumeHud(noVid); if (!noVid) { setupVideoPlayerOnwheel(); - if (!isEnabled('video-toggle')) { + if (!window.mainConfig.plugins.isEnabled('video-toggle')) { // Video-toggle handles hud positioning on its own const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; $('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode())); @@ -76,9 +73,9 @@ function firstRun() { } // Change options from renderer to keep sync - ipcRenderer.on('setOptions', (_event, newOptions = {}) => { + window.ipcRenderer.on('setOptions', (_event, newOptions = {}) => { Object.assign(options, newOptions); - setMenuOptions('precise-volume', options); + window.mainConfig.plugins.setMenuOptions('precise-volume', options); }); } diff --git a/src/plugins/quality-changer/front.ts b/src/plugins/quality-changer/front.ts index 38514113..404447dc 100644 --- a/src/plugins/quality-changer/front.ts +++ b/src/plugins/quality-changer/front.ts @@ -1,8 +1,6 @@ -import { ipcRenderer } from 'electron'; - import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html'; -import { ElementFromHtml } from '../utils'; +import { ElementFromHtml } from '../utils-renderer'; import { YoutubePlayer } from '../../types/youtube-player'; function $(selector: string): HTMLElement | null { @@ -23,7 +21,7 @@ function setup(event: CustomEvent) { const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality()); - ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => { + window.ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => { if (promise.response === -1) { return; } diff --git a/src/plugins/sponsorblock/front.ts b/src/plugins/sponsorblock/front.ts index ccb2f808..022ff950 100644 --- a/src/plugins/sponsorblock/front.ts +++ b/src/plugins/sponsorblock/front.ts @@ -1,12 +1,9 @@ -import { ipcRenderer } from 'electron'; -import is from 'electron-is'; - import { Segment } from './types'; let currentSegments: Segment[] = []; export default () => { - ipcRenderer.on('sponsorblock-skip', (_, segments: Segment[]) => { + window.ipcRenderer.on('sponsorblock-skip', (_, segments: Segment[]) => { currentSegments = segments; }); @@ -24,7 +21,7 @@ export default () => { && target.currentTime < segment[1] ) { target.currentTime = segment[1]; - if (is.dev()) { + if (window.electronIs.dev()) { console.log('SponsorBlock: skipping segment', segment); } } diff --git a/src/plugins/utils-renderer.ts b/src/plugins/utils-renderer.ts new file mode 100644 index 00000000..9b9e85ae --- /dev/null +++ b/src/plugins/utils-renderer.ts @@ -0,0 +1,8 @@ +// Creates a DOM element from an HTML string +export const ElementFromHtml = (html: string): HTMLElement => { + const template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + + return template.content.firstElementChild as HTMLElement; +}; diff --git a/src/plugins/utils.ts b/src/plugins/utils.ts index 0bd6a851..26659fca 100644 --- a/src/plugins/utils.ts +++ b/src/plugins/utils.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { app, ipcMain, ipcRenderer } from 'electron'; +import { app } from 'electron'; import is from 'electron-is'; import { ValueOf } from '../utils/type-utils'; @@ -34,31 +34,6 @@ export const saveMediaIcon = () => { } }; -// Creates a DOM element from an HTML string -export const ElementFromHtml = (html: string): HTMLElement => { - const template = document.createElement('template'); - html = html.trim(); // Never return a text node of whitespace as the result - template.innerHTML = html; - - return template.content.firstElementChild as HTMLElement; -}; - -// Creates a DOM element from a HTML file -export const ElementFromFile = (filepath: fs.PathOrFileDescriptor) => ElementFromHtml(fs.readFileSync(filepath, 'utf8')); - -export const templatePath = (pluginPath: string, name: string) => path.join(pluginPath, 'templates', name); - -export const Actions = { - NEXT: 'next', - BACK: 'back', -}; - -export const triggerAction = (channel: string, action: ValueOf, ...args: Parameters) => ipcRenderer.send(channel, action, ...args); - -export const triggerActionSync = (channel: string, action: ValueOf, ...args: Parameters): unknown => ipcRenderer.sendSync(channel, action, ...args); - -export const listenAction = (channel: string, callback: (event: Electron.IpcMainEvent, action: string) => void) => ipcMain.on(channel, callback); - export const fileExists = ( path: fs.PathLike, callbackIfExists: { (): void; (): void; (): void; }, diff --git a/src/plugins/video-toggle/front.ts b/src/plugins/video-toggle/front.ts index 39e7f6e1..6dd3a381 100644 --- a/src/plugins/video-toggle/front.ts +++ b/src/plugins/video-toggle/front.ts @@ -1,7 +1,6 @@ import buttonTemplate from './templates/button_template.html'; -import { ElementFromHtml } from '../utils'; -import { setOptions, isEnabled } from '../../config/plugins'; +import { ElementFromHtml } from '../utils-renderer'; import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/front'; @@ -10,7 +9,7 @@ import { ThumbnailElement } from '../../types/get-player-response'; import type { ConfigType } from '../../config/dynamic'; -const moveVolumeHud = isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; +const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; function $(selector: string): E | null { return document.querySelector(selector); @@ -99,7 +98,7 @@ function setup(e: CustomEvent) { function setVideoState(showVideo: boolean) { options.hideVideo = !showVideo; - setOptions('video-toggle', options); + window.mainConfig.plugins.setOptions('video-toggle', options); const checkbox = $('.video-switch-button-checkbox'); // custom mode if (checkbox) checkbox.checked = !options.hideVideo; diff --git a/src/plugins/visualizer/visualizers/butterchurn.ts b/src/plugins/visualizer/visualizers/butterchurn.ts index 5c44cc33..6d3b070e 100644 --- a/src/plugins/visualizer/visualizers/butterchurn.ts +++ b/src/plugins/visualizer/visualizers/butterchurn.ts @@ -3,7 +3,7 @@ import ButterchurnPresets from 'butterchurn-presets'; import { Visualizer } from './visualizer'; -import { ConfigType } from '../../../config/dynamic'; +import type { ConfigType } from '../../../config/dynamic'; class ButterchurnVisualizer extends Visualizer { name = 'butterchurn'; diff --git a/src/preload.ts b/src/preload.ts index 8cd32504..6b19f884 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,40 +1,14 @@ -import { ipcRenderer } from 'electron'; +import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import is from 'electron-is'; import config from './config'; -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 adblockerPreload from './plugins/adblocker/preload'; import preciseVolumePreload from './plugins/precise-volume/preload'; import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic'; -type PluginMapper = { +export type PluginMapper = { [Key in OneOfDefaultConfigKey]?: ( Type extends 'renderer' ? (options: ConfigType) => (Promise | void) : Type extends 'preload' ? () => (Promise | void) : @@ -42,31 +16,6 @@ type PluginMapper = { ) }; -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, -}; - const preloadPlugins: PluginMapper<'preload'> = { 'adblocker': adblockerPreload, 'precise-volume': preciseVolumePreload, @@ -74,10 +23,6 @@ const preloadPlugins: PluginMapper<'preload'> = { const enabledPluginNameAndOptions = config.plugins.getEnabled(); -const $ = document.querySelector.bind(document); - -let api: Element | null = null; - enabledPluginNameAndOptions.forEach(async ([plugin, options]) => { if (Object.hasOwn(preloadPlugins, plugin)) { const handler = preloadPlugins[plugin]; @@ -89,119 +34,17 @@ enabledPluginNameAndOptions.forEach(async ([plugin, options]) => { } }); -document.addEventListener('DOMContentLoaded', () => { - enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { - if (Object.hasOwn(rendererPlugins, pluginName)) { - const handler = rendererPlugins[pluginName]; - try { - await handler?.(options as never); - } catch (error) { - console.error(`Error in plugin "${pluginName}": ${String(error)}`); - } - } - }); - - // Wait for complete load of YouTube api - listenForApiLoad(); - - // Inject song-info provider - setupSongInfo(); - - // Inject song-controls - setupSongControls(); - - // Add action for reloading - window.reload = () => ipcRenderer.send('reload'); - - // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min - setInterval(() => window._lact = Date.now(), 900_000); - - // Setup back to front logger - if (is.dev()) { - ipcRenderer.on('log', (_event, log: string) => { - console.log(JSON.parse(log)); - }); - } +contextBridge.exposeInMainWorld('mainConfig', config); +contextBridge.exposeInMainWorld('electronIs', is); +contextBridge.exposeInMainWorld('ipcRenderer', { + on: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.on(channel, listener), + off: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.off(channel, listener), + once: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.once(channel, listener), + send: (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args), + removeListener: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.removeListener(channel, listener), + removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel), + invoke: async (channel: string, ...args: unknown[]): Promise => ipcRenderer.invoke(channel, ...args), + sendSync: (channel: string, ...args: unknown[]): unknown => ipcRenderer.sendSync(channel, ...args), + sendToHost: (channel: string, ...args: unknown[]) => ipcRenderer.sendToHost(channel, ...args), }); - -function listenForApiLoad() { - api = $('#movie_player'); - if (api) { - onApiLoaded(); - return; - } - - const observer = new MutationObserver(() => { - api = $('#movie_player'); - if (api) { - observer.disconnect(); - onApiLoaded(); - } - }); - - observer.observe(document.documentElement, { childList: true, subtree: true }); -} - -interface YouTubeMusicAppElement extends HTMLElement { - navigate_(page: string): void; -} - -function onApiLoaded() { - const video = $('video')!; - const audioContext = new AudioContext(); - const audioSource = audioContext.createMediaElementSource(video); - audioSource.connect(audioContext.destination); - - video.addEventListener( - 'loadstart', - () => { - // Emit "audioCanPlay" for each video - video.addEventListener( - 'canplaythrough', - () => { - document.dispatchEvent( - new CustomEvent('audioCanPlay', { - detail: { - audioContext, - audioSource, - }, - }), - ); - }, - { once: true }, - ); - }, - { passive: true }, - );! - - document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); - ipcRenderer.send('apiLoaded'); - - // Navigate to "Starting page" - const startingPage: string = config.get('options.startingPage'); - if (startingPage && startingPages[startingPage]) { - $('ytmusic-app')?.navigate_(startingPages[startingPage]); - } - - // Remove upgrade button - if (config.get('options.removeUpgradeButton')) { - const styles = document.createElement('style'); - styles.innerHTML = `ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:nth-child(4) { - display: none; - }`; - document.head.appendChild(styles); - } - - // Hide / Force show like buttons - const likeButtonsOptions: string = config.get('options.likeButtons'); - if (likeButtonsOptions) { - const likeButtons: HTMLElement | null = $('ytmusic-like-button-renderer'); - if (likeButtons) { - likeButtons.style.display - = { - hide: 'none', - force: 'inherit', - }[likeButtonsOptions] || ''; - } - } -} +contextBridge.exposeInMainWorld('reload', () => ipcRenderer.send('reload')); diff --git a/src/providers/song-controls-front.ts b/src/providers/song-controls-front.ts index 012a75b4..be080c81 100644 --- a/src/providers/song-controls-front.ts +++ b/src/providers/song-controls-front.ts @@ -1,8 +1,6 @@ -import { ipcRenderer } from 'electron'; - export const setupSongControls = () => { document.addEventListener('apiLoaded', (event) => { - ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t)); - ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t)); + window.ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t)); + window.ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t)); }, { once: true, passive: true }); }; diff --git a/src/providers/song-info-front.ts b/src/providers/song-info-front.ts index f18a3a3a..05ae3f2a 100644 --- a/src/providers/song-info-front.ts +++ b/src/providers/song-info-front.ts @@ -1,21 +1,18 @@ -import { ipcRenderer } from 'electron'; - import { singleton } from './decorators'; -import { getImage, SongInfo } from './song-info'; -import { YoutubePlayer } from '../types/youtube-player'; -import { GetState } from '../types/datahost-get-state'; -import { VideoDataChangeValue } from '../types/player-api-events'; +import type { YoutubePlayer } from '../types/youtube-player'; +import type { GetState } from '../types/datahost-get-state'; +import type { VideoDataChangeValue } from '../types/player-api-events'; + +import type { SongInfo } from './song-info'; let songInfo: SongInfo = {} as SongInfo; export const getSongInfo = () => songInfo; const $ = (s: string): E | null => document.querySelector(s); -const $$ = (s: string): NodeListOf => document.querySelectorAll(s); -ipcRenderer.on('update-song-info', async (_, extractedSongInfo: SongInfo) => { +window.ipcRenderer.on('update-song-info', (_, extractedSongInfo: SongInfo) => { songInfo = extractedSongInfo; - if (songInfo.imageSrc) songInfo.image = await getImage(songInfo.imageSrc); }); // Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473) @@ -24,7 +21,7 @@ const srcChangedEvent = new CustomEvent('srcChanged'); export const setupSeekedListener = singleton(() => { $('video')?.addEventListener('seeked', (v) => { if (v.target instanceof HTMLVideoElement) { - ipcRenderer.send('seeked', v.target.currentTime); + window.ipcRenderer.send('seeked', v.target.currentTime); } }); }); @@ -33,7 +30,7 @@ export const setupTimeChangedListener = singleton(() => { const progressObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { const target = mutation.target as Node & { value: string }; - ipcRenderer.send('timeChanged', target.value); + window.ipcRenderer.send('timeChanged', target.value); songInfo.elapsedSeconds = Number(target.value); } }); @@ -47,7 +44,7 @@ export const setupRepeatChangedListener = singleton(() => { const repeatObserver = new MutationObserver((mutations) => { // provided by YouTube Music - ipcRenderer.send( + window.ipcRenderer.send( 'repeatChanged', (mutations[0].target as Node & { __dataHost: { @@ -60,7 +57,7 @@ export const setupRepeatChangedListener = singleton(() => { // Emit the initial value as well; as it's persistent between launches. // provided by YouTube Music - ipcRenderer.send( + window.ipcRenderer.send( 'repeatChanged', $ GetState; @@ -70,33 +67,33 @@ export const setupRepeatChangedListener = singleton(() => { export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => { $('video')?.addEventListener('volumechange', () => { - ipcRenderer.send('volumeChanged', api.getVolume()); + window.ipcRenderer.send('volumeChanged', api.getVolume()); }); // Emit the initial value as well; as it's persistent between launches. - ipcRenderer.send('volumeChanged', api.getVolume()); + window.ipcRenderer.send('volumeChanged', api.getVolume()); }); export default () => { document.addEventListener('apiLoaded', (apiEvent) => { - ipcRenderer.on('setupTimeChangedListener', () => { + window.ipcRenderer.on('setupTimeChangedListener', () => { setupTimeChangedListener(); }); - ipcRenderer.on('setupRepeatChangedListener', () => { + window.ipcRenderer.on('setupRepeatChangedListener', () => { setupRepeatChangedListener(); }); - ipcRenderer.on('setupVolumeChangedListener', () => { + window.ipcRenderer.on('setupVolumeChangedListener', () => { setupVolumeChangedListener(apiEvent.detail); }); - ipcRenderer.on('setupSeekedListener', () => { + window.ipcRenderer.on('setupSeekedListener', () => { setupSeekedListener(); }); const playPausedHandler = (e: Event, status: string) => { if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) { - ipcRenderer.send('playPaused', { + window.ipcRenderer.send('playPaused', { isPaused: status === 'pause', elapsedSeconds: Math.floor(e.target.currentTime), }); @@ -143,7 +140,7 @@ export default () => { data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name; } - ipcRenderer.send('video-src-changed', data); + window.ipcRenderer.send('video-src-changed', data); } }, { once: true, passive: true }); }; diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 00000000..03066225 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,171 @@ +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, +}; + +const enabledPluginNameAndOptions = window.mainConfig.plugins.getEnabled(); + +let api: Element | null = null; + +function listenForApiLoad() { + api = document.querySelector('#movie_player'); + if (api) { + onApiLoaded(); + return; + } + + const observer = new MutationObserver(() => { + api = document.querySelector('#movie_player'); + if (api) { + observer.disconnect(); + onApiLoaded(); + } + }); + + observer.observe(document.documentElement, { childList: true, subtree: true }); +} + +interface YouTubeMusicAppElement extends HTMLElement { + navigate_(page: string): void; +} + +function onApiLoaded() { + const video = document.querySelector('video')!; + const audioContext = new AudioContext(); + const audioSource = audioContext.createMediaElementSource(video); + audioSource.connect(audioContext.destination); + + video.addEventListener( + 'loadstart', + () => { + // Emit "audioCanPlay" for each video + video.addEventListener( + 'canplaythrough', + () => { + document.dispatchEvent( + new CustomEvent('audioCanPlay', { + detail: { + audioContext, + audioSource, + }, + }), + ); + }, + { once: true }, + ); + }, + { passive: true }, + );! + + document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); + window.ipcRenderer.send('apiLoaded'); + + // Navigate to "Starting page" + const startingPage: string = window.mainConfig.get('options.startingPage'); + if (startingPage && startingPages[startingPage]) { + document.querySelector('ytmusic-app')?.navigate_(startingPages[startingPage]); + } + + // Remove upgrade button + if (window.mainConfig.get('options.removeUpgradeButton')) { + const styles = document.createElement('style'); + styles.innerHTML = `ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:last-child { + display: none; + }`; + document.head.appendChild(styles); + } + + // Hide / Force show like buttons + const likeButtonsOptions: string = window.mainConfig.get('options.likeButtons'); + if (likeButtonsOptions) { + const likeButtons: HTMLElement | null = document.querySelector('ytmusic-like-button-renderer'); + if (likeButtons) { + likeButtons.style.display + = { + hide: 'none', + force: 'inherit', + }[likeButtonsOptions] || ''; + } + } +} + +(() => { + enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { + if (Object.hasOwn(rendererPlugins, pluginName)) { + const handler = rendererPlugins[pluginName]; + try { + await handler?.(options as never); + } catch (error) { + console.error(`Error in plugin "${pluginName}": ${String(error)}`); + } + } + }); + + // Inject song-info provider + setupSongInfo(); + + // Inject song-controls + setupSongControls(); + + // Wait for complete load of YouTube api + listenForApiLoad(); + + // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min + setInterval(() => window._lact = Date.now(), 900_000); + + // Setup back to front logger + if (window.electronIs.dev()) { + window.ipcRenderer.on('log', (_event, log: string) => { + console.log(JSON.parse(log)); + }); + } +})(); diff --git a/src/reset.d.ts b/src/reset.d.ts index 718c3de9..ae20968b 100644 --- a/src/reset.d.ts +++ b/src/reset.d.ts @@ -1,4 +1,9 @@ import '@total-typescript/ts-reset'; + +import { ipcRenderer as electronIpcRenderer } from 'electron'; +import is from 'electron-is'; + +import config from './config'; import { YoutubePlayer } from './types/youtube-player'; declare global { @@ -13,6 +18,9 @@ declare global { } interface Window { + ipcRenderer: typeof electronIpcRenderer; + mainConfig: typeof config; + electronIs: typeof is; /** * YouTube Music internal variable (Last interaction time) */