mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
feat: enable context-isolation (#1361)
This commit is contained in:
182
src/config/dynamic-renderer.ts
Normal file
182
src/config/dynamic-renderer.ts
Normal file
@ -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<any> } = {};
|
||||
|
||||
export const getActivePlugins
|
||||
= async () => await window.ipcRenderer.invoke('get-active-plugins') as Promise<typeof activePlugins>;
|
||||
|
||||
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> = T[keyof T];
|
||||
|
||||
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||
private readonly name: string;
|
||||
private readonly config: ConfigType<T>;
|
||||
private readonly defaultConfig: ConfigType<T>;
|
||||
private readonly enableFront: boolean;
|
||||
|
||||
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
|
||||
private allSubscribers: ((config: ConfigType<T>) => 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<Key extends keyof ConfigType<T> = keyof ConfigType<T>>(key: Key): ConfigType<T>[Key] {
|
||||
return this.config?.[key];
|
||||
}
|
||||
|
||||
set(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
|
||||
this.config[key] = value;
|
||||
this.onChange(key);
|
||||
this.save();
|
||||
}
|
||||
|
||||
getAll(): ConfigType<T> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
setAll(options: Partial<ConfigType<T>>) {
|
||||
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<typeof options>) {
|
||||
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<T>, value: ValueOf<ConfigType<T>>) {
|
||||
this.config[key] = value;
|
||||
window.mainConfig.plugins.setMenuOptions(this.name, this.config);
|
||||
this.onChange(key);
|
||||
}
|
||||
|
||||
subscribe(valueName: keyof ConfigType<T>, fn: (config: ConfigType<T>) => void) {
|
||||
this.subscribers[valueName] = fn;
|
||||
}
|
||||
|
||||
subscribeAll(fn: (config: ConfigType<T>) => void) {
|
||||
this.allSubscribers.push(fn);
|
||||
}
|
||||
|
||||
/** Called only from back */
|
||||
private save() {
|
||||
window.mainConfig.plugins.setOptions(this.name, this.config);
|
||||
}
|
||||
|
||||
private onChange(valueName: keyof ConfigType<T>, single: boolean = true) {
|
||||
this.subscribers[valueName]?.(this.config[valueName] as ConfigType<T>);
|
||||
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<this>) {
|
||||
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<T>) => 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<T>) => {
|
||||
fn(value);
|
||||
},
|
||||
);
|
||||
window.ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
|
||||
};
|
||||
|
||||
this.subscribeAll = (fn: (config: ConfigType<T>) => void) => {
|
||||
window.ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType<T>) => {
|
||||
fn(value);
|
||||
});
|
||||
window.ipcRenderer.send(`${this.name}-config-subscribe-all`);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<any> } = {};
|
||||
|
||||
/**
|
||||
* [!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<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
|
||||
type ValueOf<T> = T[keyof T];
|
||||
type Mode<T, Mode extends 'r' | 'm'> = Mode extends 'r' ? Promise<T> : T;
|
||||
|
||||
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||
private readonly name: string;
|
||||
private readonly config: ConfigType<T>;
|
||||
@ -180,62 +159,25 @@ export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||
private setupFront() {
|
||||
const ignoredMethods = ['subscribe', 'subscribeAll'];
|
||||
|
||||
if (process.type === 'renderer') {
|
||||
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||
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<T>) => 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<T>) => {
|
||||
fn(value);
|
||||
},
|
||||
);
|
||||
ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
|
||||
};
|
||||
|
||||
this.subscribeAll = (fn: (config: ConfigType<T>) => void) => {
|
||||
ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType<T>) => {
|
||||
fn(value);
|
||||
});
|
||||
ipcRenderer.send(`${this.name}-config-subscribe-all`);
|
||||
};
|
||||
}
|
||||
} else if (process.type === 'browser') {
|
||||
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||
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<this>) {
|
||||
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType<T>) => {
|
||||
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<T>) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<DefaultPluginsConfig>).filter(([plugin]) =>
|
||||
isEnabled(plugin),
|
||||
const plugins = deepmergeFn(defaultConfig.plugins, (store.get('plugins') as DefaultPluginsConfig));
|
||||
return (Object.entries(plugins) as Entries<DefaultPluginsConfig>).filter(([, options]) =>
|
||||
(options as Plugin).enabled,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
15
src/index.ts
15
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;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/await-thenable */
|
||||
/* renderer */
|
||||
|
||||
import { blockers } from './blocker-types';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<HTMLElement>('#player-page');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#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 });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
4
src/plugins/captions-selector/config-renderer.ts
Normal file
4
src/plugins/captions-selector/config-renderer.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PluginConfig } from '../../config/dynamic-renderer';
|
||||
|
||||
const configRenderer = new PluginConfig('captions-selector', { enableFront: true });
|
||||
export default configRenderer;
|
||||
@ -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 $ = <Element extends HTMLElement>(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<LanguageOptions[]>('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;
|
||||
}
|
||||
|
||||
4
src/plugins/crossfade/config-renderer.ts
Normal file
4
src/plugins/crossfade/config-renderer.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PluginConfig } from '../../config/dynamic-renderer';
|
||||
|
||||
const config = new PluginConfig('crossfade', { enableFront: true });
|
||||
export default config;
|
||||
@ -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<unknown>;
|
||||
|
||||
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<string>;
|
||||
const getStreamURL = async (videoID: string) => window.ipcRenderer.invoke('audio-url', videoID) as Promise<string>;
|
||||
|
||||
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, {
|
||||
|
||||
@ -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<HTMLAnchorElement>('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<HTMLAnchorElement>('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 {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@ -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 $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(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<HTMLDivElement>('#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();
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
})
|
||||
|
||||
@ -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?.();
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 = $<HTMLElement>('.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());
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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<YoutubePlayer>) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
8
src/plugins/utils-renderer.ts
Normal file
8
src/plugins/utils-renderer.ts
Normal file
@ -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;
|
||||
};
|
||||
@ -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 = <Parameters extends unknown[]>(channel: string, action: ValueOf<typeof Actions>, ...args: Parameters) => ipcRenderer.send(channel, action, ...args);
|
||||
|
||||
export const triggerActionSync = <Parameters extends unknown[]>(channel: string, action: ValueOf<typeof Actions>, ...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; },
|
||||
|
||||
@ -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 $<E extends Element = Element>(selector: string): E | null {
|
||||
return document.querySelector<E>(selector);
|
||||
@ -99,7 +98,7 @@ function setup(e: CustomEvent<YoutubePlayer>) {
|
||||
|
||||
function setVideoState(showVideo: boolean) {
|
||||
options.hideVideo = !showVideo;
|
||||
setOptions('video-toggle', options);
|
||||
window.mainConfig.plugins.setOptions('video-toggle', options);
|
||||
|
||||
const checkbox = $<HTMLInputElement>('.video-switch-button-checkbox'); // custom mode
|
||||
if (checkbox) checkbox.checked = !options.hideVideo;
|
||||
|
||||
@ -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<Butterchurn> {
|
||||
name = 'butterchurn';
|
||||
|
||||
187
src/preload.ts
187
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<Type extends 'renderer' | 'preload' | 'backend'> = {
|
||||
export type PluginMapper<Type extends 'renderer' | 'preload' | 'backend'> = {
|
||||
[Key in OneOfDefaultConfigKey]?: (
|
||||
Type extends 'renderer' ? (options: ConfigType<Key>) => (Promise<void> | void) :
|
||||
Type extends 'preload' ? () => (Promise<void> | void) :
|
||||
@ -42,31 +16,6 @@ type PluginMapper<Type extends 'renderer' | 'preload' | 'backend'> = {
|
||||
)
|
||||
};
|
||||
|
||||
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<unknown> => 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]) {
|
||||
$<YouTubeMusicAppElement>('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'));
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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 $ = <E extends Element = Element>(s: string): E | null => document.querySelector<E>(s);
|
||||
const $$ = <E extends Element = Element>(s: string): NodeListOf<E> => document.querySelectorAll<E>(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',
|
||||
$<HTMLElement & {
|
||||
getState: () => 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 });
|
||||
};
|
||||
|
||||
171
src/renderer.ts
Normal file
171
src/renderer.ts
Normal file
@ -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<YouTubeMusicAppElement>('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));
|
||||
});
|
||||
}
|
||||
})();
|
||||
8
src/reset.d.ts
vendored
8
src/reset.d.ts
vendored
@ -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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user