feat(plugin): migrate some plugin (WIP)

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Su-Yong
2023-11-11 00:03:26 +09:00
parent e0e17cac99
commit 5cd1d9abe8
29 changed files with 938 additions and 796 deletions

View File

@ -11,6 +11,7 @@ export default defineConfig({
const commonConfig: UserConfig = { const commonConfig: UserConfig = {
plugins: [ plugins: [
viteResolve({ viteResolve({
'virtual:PluginBuilders': pluginVirtualModuleGenerator('index'),
'virtual:MainPlugins': pluginVirtualModuleGenerator('main'), 'virtual:MainPlugins': pluginVirtualModuleGenerator('main'),
'virtual:MenuPlugins': pluginVirtualModuleGenerator('menu'), 'virtual:MenuPlugins': pluginVirtualModuleGenerator('menu'),
}), }),

View File

@ -11,6 +11,7 @@ const set = (key: string, value: unknown) => {
store.set(key, value); store.set(key, value);
}; };
const setPartial = (value: object) => { const setPartial = (value: object) => {
// deepmerge(store.get, value);
store.set(value); store.set(value);
}; };

View File

@ -20,16 +20,15 @@ import { setupSongInfo } from './providers/song-info';
import { restart, setupAppControls } from './providers/app-controls'; import { restart, setupAppControls } from './providers/app-controls';
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler'; import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
// eslint-disable-next-line import/order /* eslint-disable import/order */
import { mainPlugins } from 'virtual:MainPlugins'; import { mainPlugins } from 'virtual:MainPlugins';
import ambientModeMainPluginBuilder from './plugins/ambient-mode/index'; import { pluginBuilders } from 'virtual:PluginBuilders';
import qualityChangerMainPluginBuilder from './plugins/quality-changer/index'; /* eslint-enable import/order */
import qualityChangerMainPlugin from './plugins/quality-changer/main';
import { setOptions as pipSetOptions } from './plugins/picture-in-picture/main'; import { setOptions as pipSetOptions } from './plugins/picture-in-picture/main';
import youtubeMusicCSS from './youtube-music.css'; import youtubeMusicCSS from './youtube-music.css';
import { MainPlugin, PluginBaseConfig, MainPluginContext } from './plugins/utils/builder'; import { MainPlugin, PluginBaseConfig, MainPluginContext, MainPluginFactory } from './plugins/utils/builder';
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
@ -100,16 +99,8 @@ if (is.windows()) {
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins)); ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
const pluginBuilderList = [
['ambient-mode', ambientModeMainPluginBuilder] as const,
['quality-changer', qualityChangerMainPluginBuilder] as const,
];
const mainPluginList = [
['quality-changer', qualityChangerMainPlugin] as const,
];
const initHook = (win: BrowserWindow) => { const initHook = (win: BrowserWindow) => {
ipcMain.handle('get-config', (_, name: string) => config.get(`plugins.${name}` as never)); ipcMain.handle('get-config', (_, id: keyof PluginBuilderList) => config.get(`plugins.${id}` as never) ?? pluginBuilders[id].config);
ipcMain.handle('set-config', (_, name: string, obj: object) => config.setPartial({ ipcMain.handle('set-config', (_, name: string, obj: object) => config.setPartial({
plugins: { plugins: {
[name]: obj, [name]: obj,
@ -118,11 +109,11 @@ const initHook = (win: BrowserWindow) => {
config.watch((newValue) => { config.watch((newValue) => {
const value = newValue as Record<string, unknown>; const value = newValue as Record<string, unknown>;
const target = pluginBuilderList.find(([name]) => name in value); const id = Object.keys(pluginBuilders).find((id) => id in value);
if (target) { if (id) {
win.webContents.send('config-changed', target[0], value[target[0]]); win.webContents.send('config-changed', id, value[id]);
console.log('config-changed', target[0], value[target[0]]); // console.log('config-changed', id, value[id]);
} }
}); });
}; };
@ -186,36 +177,41 @@ async function loadPlugins(win: BrowserWindow) {
}); });
for (const [plugin, options] of config.plugins.getEnabled()) { for (const [pluginId, options] of config.plugins.getEnabled()) {
const builderTarget = pluginBuilderList.find(([name]) => name === plugin); const builder = pluginBuilders[pluginId as keyof PluginBuilderList];
const mainPluginTarget = mainPluginList.find(([name]) => name === plugin); const factory = (mainPlugins as Record<string, MainPluginFactory<PluginBaseConfig>>)[pluginId];
if (mainPluginTarget) { if (builder) {
const mainPlugin = mainPluginTarget[1];
const context = createContext(mainPluginTarget[0]);
const plugin = await mainPlugin(context);
loadedPluginList.push([mainPluginTarget[0], plugin]);
plugin.onLoad?.(win);
}
if (builderTarget) {
const builder = builderTarget[1];
builder.styles?.forEach((style) => { builder.styles?.forEach((style) => {
injectCSS(win.webContents, style); injectCSS(win.webContents, style);
console.log('[YTMusic]', `"${pluginId}" plugin meta data is loaded`);
}); });
} }
if (factory) {
try { try {
if (Object.hasOwn(mainPlugins, plugin)) { const context = createContext(pluginId as keyof PluginBuilderList);
console.log('Loaded plugin - ' + plugin); const plugin = await factory(context);
const handler = mainPlugins[plugin as keyof typeof mainPlugins]; loadedPluginList.push([pluginId, plugin]);
if (handler) { plugin.onLoad?.(win);
await handler(win, options as never); console.log('[YTMusic]', `"${pluginId}" plugin is loaded`);
} catch (error) {
console.error('[YTMusic]', `Cannot load plugin "${pluginId}"`);
console.trace(error);
} }
} }
} catch (e) {
console.error(`Failed to load plugin "${plugin}"`, e); // try {
} // if (Object.hasOwn(mainPlugins, plugin)) {
// console.log('Loaded plugin - ' + plugin);
// const handler = mainPlugins[plugin as keyof typeof mainPlugins];
// if (handler) {
// await handler(win, options as never);
// }
// }
// } catch (e) {
// console.error(`Failed to load plugin "${plugin}"`, e);
// }
} }
} }

View File

@ -1,5 +1,5 @@
import is from 'electron-is'; import is from 'electron-is';
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu } from 'electron'; import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { restart } from './providers/app-controls'; import { restart } from './providers/app-controls';
@ -7,13 +7,13 @@ import config from './config';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import promptOptions from './providers/prompt-options'; import promptOptions from './providers/prompt-options';
// eslint-disable-next-line import/order /* eslint-disable import/order */
import { menuPlugins as menuList } from 'virtual:MenuPlugins'; import { menuPlugins as menuList } from 'virtual:MenuPlugins';
import { pluginBuilders } from 'virtual:PluginBuilders';
import ambientModeMenuPlugin from './plugins/ambient-mode/menu'; /* eslint-enable import/order */
import { getAvailablePluginNames } from './plugins/utils/main'; import { getAvailablePluginNames } from './plugins/utils/main';
import { PluginBaseConfig, PluginContext } from './plugins/utils/builder'; import { MenuPluginContext, MenuPluginFactory, PluginBaseConfig, PluginContext } from './plugins/utils/builder';
export type MenuTemplate = Electron.MenuItemConstructorOptions[]; export type MenuTemplate = Electron.MenuItemConstructorOptions[];
@ -48,7 +48,7 @@ export const refreshMenu = (win: BrowserWindow) => {
export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate> => { export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate> => {
const innerRefreshMenu = () => refreshMenu(win); const innerRefreshMenu = () => refreshMenu(win);
const createContext = <Config extends PluginBaseConfig>(name: string): PluginContext<Config> => ({ const createContext = <Config extends PluginBaseConfig>(name: string): MenuPluginContext<Config> => ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
getConfig: () => config.get(`plugins.${name}`) as unknown as Config, getConfig: () => config.get(`plugins.${name}`) as unknown as Config,
@ -67,11 +67,13 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
return Promise.resolve(); return Promise.resolve();
}, },
window: win,
}); });
const pluginMenus = await Promise.all( const availablePlugins = getAvailablePluginNames();
[['ambient-mode', ambientModeMenuPlugin] as const].map(async ([id, plugin]) => { const menuResult = await Promise.allSettled(
let pluginLabel = id; availablePlugins.map(async (id) => {
let pluginLabel = pluginBuilders[id as keyof PluginBuilderList]?.name ?? id;
if (betaPlugins.includes(pluginLabel)) { if (betaPlugins.includes(pluginLabel)) {
pluginLabel += ' [beta]'; pluginLabel += ' [beta]';
} }
@ -80,7 +82,8 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu); return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
} }
const template = await plugin(createContext(id)); const factory = menuList[id] as MenuPluginFactory<PluginBaseConfig>;
const template = await factory(createContext(id));
return { return {
label: pluginLabel, label: pluginLabel,
@ -93,37 +96,18 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
}), }),
); );
const pluginMenus = menuResult.map((it, index) => {
if (it.status === 'fulfilled') return it.value;
const id = availablePlugins[index];
const pluginLabel = pluginBuilders[id as keyof PluginBuilderList]?.name ?? id;
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
});
return [ return [
{ {
label: 'Plugins', label: 'Plugins',
submenu: [ submenu: pluginMenus,
...getAvailablePluginNames().map((pluginName) => {
let pluginLabel = pluginName;
if (betaPlugins.includes(pluginLabel)) {
pluginLabel += ' [beta]';
}
if (Object.hasOwn(menuList, pluginName)) {
const getPluginMenu = menuList[pluginName];
if (!config.plugins.isEnabled(pluginName)) {
return pluginEnabledMenu(pluginName, pluginLabel, true, innerRefreshMenu);
}
return {
label: pluginLabel,
submenu: [
pluginEnabledMenu(pluginName, 'Enabled', true, innerRefreshMenu),
{ type: 'separator' },
...(getPluginMenu(win, config.plugins.getOptions(pluginName), innerRefreshMenu) as MenuTemplate),
],
} satisfies Electron.MenuItemConstructorOptions;
}
return pluginEnabledMenu(pluginName, pluginLabel);
}),
...pluginMenus,
],
}, },
{ {
label: 'Options', label: 'Options',

View File

@ -0,0 +1,16 @@
import { createPluginBuilder } from '../utils/builder';
const builder = createPluginBuilder('audio-compressor', {
name: 'Audio Compressor',
config: {
enabled: false,
},
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -1,4 +1,8 @@
export default () => import builder from '.';
export default builder.createRenderer(() => {
return {
onLoad() {
document.addEventListener('audioCanPlay', (e) => { document.addEventListener('audioCanPlay', (e) => {
const { audioContext } = e.detail; const { audioContext } = e.detail;
@ -15,3 +19,6 @@ export default () =>
once: true, // Only create the audio compressor once, not on each video once: true, // Only create the audio compressor once, not on each video
passive: true, passive: true,
}); });
}
};
});

View File

@ -0,0 +1,19 @@
import style from './style.css?inline';
import { createPluginBuilder } from '../utils/builder';
const builder = createPluginBuilder('blur-nav-bar', {
name: 'Blur Navigation Bar',
config: {
enabled: false,
},
styles: [style],
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -1,9 +0,0 @@
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils/main';
export default (win: BrowserWindow) => {
injectCSS(win.webContents, style);
};

View File

@ -0,0 +1,16 @@
import { createPluginBuilder } from '../utils/builder';
const builder = createPluginBuilder('bypass-age-restrictions', {
name: 'Bypass Age Restrictions',
config: {
enabled: false,
},
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -1,4 +1,8 @@
export default async () => { import builder from '.';
export default builder.createRenderer(() => ({
async onLoad() {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
await import('simple-youtube-age-restriction-bypass'); await import('simple-youtube-age-restriction-bypass');
}; },
}));

View File

@ -0,0 +1,20 @@
import titlebarStyle from './titlebar.css?inline';
import { createPluginBuilder } from '../utils/builder';
export const builder = createPluginBuilder('in-app-menu', {
name: 'In-App Menu',
config: {
enabled: false,
hideDOMWindowControls: false,
},
styles: [titlebarStyle],
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -2,14 +2,12 @@ import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron'; import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
import titlebarStyle from './titlebar.css'; import builder from './';
import { injectCSS } from '../utils/main'; export default builder.createMain(({ handle }) => {
// Tracks menu visibility
export default (win: BrowserWindow) => {
injectCSS(win.webContents, titlebarStyle);
return {
onLoad(win) {
win.on('close', () => { win.on('close', () => {
win.webContents.send('close-all-in-app-menu-panel'); win.webContents.send('close-all-in-app-menu-panel');
}); });
@ -20,7 +18,7 @@ export default (win: BrowserWindow) => {
}); });
}); });
ipcMain.handle( handle(
'get-menu', 'get-menu',
() => JSON.parse(JSON.stringify( () => JSON.parse(JSON.stringify(
Menu.getApplicationMenu(), Menu.getApplicationMenu(),
@ -51,7 +49,7 @@ export default (win: BrowserWindow) => {
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender); if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
}); });
ipcMain.handle('get-menu-by-id', (_, commandId: number) => { handle('get-menu-by-id', (_, commandId: number) => {
const result = getMenuItemById(commandId); const result = getMenuItemById(commandId);
return JSON.parse(JSON.stringify( return JSON.parse(JSON.stringify(
@ -60,17 +58,19 @@ export default (win: BrowserWindow) => {
); );
}); });
ipcMain.handle('window-is-maximized', () => win.isMaximized()); handle('window-is-maximized', () => win.isMaximized());
ipcMain.handle('window-close', () => win.close()); handle('window-close', () => win.close());
ipcMain.handle('window-minimize', () => win.minimize()); handle('window-minimize', () => win.minimize());
ipcMain.handle('window-maximize', () => win.maximize()); handle('window-maximize', () => win.maximize());
win.on('maximize', () => win.webContents.send('window-maximize')); win.on('maximize', () => win.webContents.send('window-maximize'));
ipcMain.handle('window-unmaximize', () => win.unmaximize()); handle('window-unmaximize', () => win.unmaximize());
win.on('unmaximize', () => win.webContents.send('window-unmaximize')); win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
ipcMain.handle('image-path-to-data-url', (_, imagePath: string) => { handle('image-path-to-data-url', (_, imagePath: string) => {
const nativeImageIcon = nativeImage.createFromPath(imagePath); const nativeImageIcon = nativeImage.createFromPath(imagePath);
return nativeImageIcon?.toDataURL(); return nativeImageIcon?.toDataURL();
}); });
}; },
};
});

View File

@ -1,14 +1,14 @@
import { BrowserWindow } from 'electron';
import is from 'electron-is'; import is from 'electron-is';
import builder from './';
import { setMenuOptions } from '../../config/plugins'; import { setMenuOptions } from '../../config/plugins';
import type { MenuTemplate } from '../../menu'; export default builder.createMenu(async ({ getConfig }) => {
import type { ConfigType } from '../../config/dynamic'; const config = await getConfig();
export default (_: BrowserWindow, config: ConfigType<'in-app-menu'>): MenuTemplate => [ if (is.linux()) {
...(is.linux() ? [ return [
{ {
label: 'Hide DOM Window Controls', label: 'Hide DOM Window Controls',
type: 'checkbox', type: 'checkbox',
@ -18,5 +18,8 @@ export default (_: BrowserWindow, config: ConfigType<'in-app-menu'>): MenuTempla
setMenuOptions('in-app-menu', config); setMenuOptions('in-app-menu', config);
} }
} }
] : []) satisfies Electron.MenuItemConstructorOptions[], ];
]; }
return [];
});

View File

@ -6,17 +6,20 @@ import minimizeRaw from './assets/minimize.svg?inline';
import maximizeRaw from './assets/maximize.svg?inline'; import maximizeRaw from './assets/maximize.svg?inline';
import unmaximizeRaw from './assets/unmaximize.svg?inline'; import unmaximizeRaw from './assets/unmaximize.svg?inline';
import type { Menu } from 'electron'; import builder from './index';
function $<E extends Element = Element>(selector: string) { import type { Menu } from 'electron';
return document.querySelector<E>(selector);
}
const isMacOS = navigator.userAgent.includes('Macintosh'); const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
export default async () => { export default builder.createRenderer(({ getConfig, invoke, on }) => {
const hideDOMWindowControls = window.mainConfig.get('plugins.in-app-menu.hideDOMWindowControls'); return {
async onLoad() {
const config = await getConfig();
const hideDOMWindowControls = config.hideDOMWindowControls;
let hideMenu = window.mainConfig.get('options.hideMenu'); let hideMenu = window.mainConfig.get('options.hideMenu');
const titleBar = document.createElement('title-bar'); const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background'); const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
@ -60,7 +63,7 @@ export default async () => {
}; };
logo.onclick = logoClick; logo.onclick = logoClick;
window.ipcRenderer.on('toggle-in-app-menu', logoClick); on('toggle-in-app-menu', logoClick);
if (!isMacOS) titleBar.appendChild(logo); if (!isMacOS) titleBar.appendChild(logo);
document.body.appendChild(titleBar); document.body.appendChild(titleBar);
@ -73,10 +76,10 @@ export default async () => {
const minimizeButton = document.createElement('button'); const minimizeButton = document.createElement('button');
minimizeButton.classList.add('window-control'); minimizeButton.classList.add('window-control');
minimizeButton.appendChild(minimize); minimizeButton.appendChild(minimize);
minimizeButton.onclick = () => window.ipcRenderer.invoke('window-minimize'); minimizeButton.onclick = () => invoke('window-minimize');
maximizeButton = document.createElement('button'); maximizeButton = document.createElement('button');
if (await window.ipcRenderer.invoke('window-is-maximized')) { if (await invoke('window-is-maximized')) {
maximizeButton.classList.add('window-control'); maximizeButton.classList.add('window-control');
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
} else { } else {
@ -84,27 +87,27 @@ export default async () => {
maximizeButton.appendChild(maximize); maximizeButton.appendChild(maximize);
} }
maximizeButton.onclick = async () => { maximizeButton.onclick = async () => {
if (await window.ipcRenderer.invoke('window-is-maximized')) { if (await invoke('window-is-maximized')) {
// change icon to maximize // change icon to maximize
maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(maximize); maximizeButton.appendChild(maximize);
// call unmaximize // call unmaximize
await window.ipcRenderer.invoke('window-unmaximize'); await invoke('window-unmaximize');
} else { } else {
// change icon to unmaximize // change icon to unmaximize
maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
// call maximize // call maximize
await window.ipcRenderer.invoke('window-maximize'); await invoke('window-maximize');
} }
}; };
const closeButton = document.createElement('button'); const closeButton = document.createElement('button');
closeButton.classList.add('window-control'); closeButton.classList.add('window-control');
closeButton.appendChild(close); closeButton.appendChild(close);
closeButton.onclick = () => window.ipcRenderer.invoke('window-close'); closeButton.onclick = () => invoke('window-close');
// Create a container div for the window control buttons // Create a container div for the window control buttons
const windowControlsContainer = document.createElement('div'); const windowControlsContainer = document.createElement('div');
@ -137,7 +140,7 @@ export default async () => {
}); });
panelClosers = []; panelClosers = [];
const menu = await window.ipcRenderer.invoke('get-menu') as Menu | null; const menu = await invoke<Menu | null>('get-menu');
if (!menu) return; if (!menu) return;
menu.items.forEach((menuItem) => { menu.items.forEach((menuItem) => {
@ -157,17 +160,17 @@ export default async () => {
document.title = 'Youtube Music'; document.title = 'Youtube Music';
window.ipcRenderer.on('close-all-in-app-menu-panel', () => { on('close-all-in-app-menu-panel', () => {
panelClosers.forEach((closer) => closer()); panelClosers.forEach((closer) => closer());
}); });
window.ipcRenderer.on('refresh-in-app-menu', () => updateMenu()); on('refresh-in-app-menu', () => updateMenu());
window.ipcRenderer.on('window-maximize', () => { on('window-maximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
} }
}); });
window.ipcRenderer.on('window-unmaximize', () => { on('window-unmaximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
@ -175,17 +178,19 @@ export default async () => {
}); });
if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { if (window.mainConfig.plugins.isEnabled('picture-in-picture')) {
window.ipcRenderer.on('pip-toggle', () => { on('pip-toggle', () => {
updateMenu(); updateMenu();
}); });
} }
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', () => {
const htmlHeadStyle = $('head > div > style'); const htmlHeadStyle = document.querySelector('head > div > style');
if (htmlHeadStyle) { if (htmlHeadStyle) {
// HACK: This is a hack to remove the scrollbar width // HACK: This is a hack to remove the scrollbar width
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {');
} }
}, { once: true, passive: true }); }, { once: true, passive: true });
}; }
};
});

View File

@ -1,18 +0,0 @@
import forwardHTML from './templates/forward.html?raw';
import backHTML from './templates/back.html?raw';
import { ElementFromHtml } from '../utils/renderer';
export function run() {
window.ipcRenderer.on('navigation-css-ready', () => {
const forwardButton = ElementFromHtml(forwardHTML);
const backButton = ElementFromHtml(backHTML);
const menu = document.querySelector('#right-content');
if (menu) {
menu.prepend(backButton, forwardButton);
}
});
}
export default run;

View File

@ -0,0 +1,19 @@
import style from './style.css?inline';
import { createPluginBuilder } from '../utils/builder';
export const builder = createPluginBuilder('navigation', {
name: 'Navigation',
config: {
enabled: false,
},
styles: [style],
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -1,13 +0,0 @@
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils/main';
export function handle(win: BrowserWindow) {
injectCSS(win.webContents, style, () => {
win.webContents.send('navigation-css-ready');
});
}
export default handle;

View File

@ -0,0 +1,20 @@
import forwardHTML from './templates/forward.html?raw';
import backHTML from './templates/back.html?raw';
import builder from '.';
import { ElementFromHtml } from '../utils/renderer';
export default builder.createRenderer(() => {
return {
onLoad() {
const forwardButton = ElementFromHtml(forwardHTML);
const backButton = ElementFromHtml(backHTML);
const menu = document.querySelector('#right-content');
if (menu) {
menu.prepend(backButton, forwardButton);
}
}
};
});

View File

@ -0,0 +1,37 @@
import hudStyle from './volume-hud.css?inline';
import { createPluginBuilder } from '../utils/builder';
export type PreciseVolumePluginConfig = {
enabled: boolean;
steps: number;
arrowsShortcut: boolean;
globalShortcuts: {
volumeUp: string;
volumeDown: string;
};
savedVolume: number | undefined;
};
const builder = createPluginBuilder('precise-volume', {
name: 'Precise Volume',
config: {
enabled: false,
steps: 1, // Percentage of volume to change
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
volumeUp: '',
volumeDown: '',
},
savedVolume: undefined, // Plugin save volume between session here
} as PreciseVolumePluginConfig,
styles: [hudStyle],
});
export default builder;
declare global {
interface PluginBuilderList {
[builder.id]: typeof builder;
}
}

View File

@ -1,28 +1,19 @@
import { globalShortcut, BrowserWindow } from 'electron'; import { globalShortcut } from 'electron';
import volumeHudStyle from './volume-hud.css'; import builder from '.';
import { injectCSS } from '../utils/main'; export default builder.createMain(({ getConfig, send }) => {
return {
async onLoad() {
const config = await getConfig();
import type { ConfigType } from '../../config/dynamic'; if (config.globalShortcuts?.volumeUp) {
globalShortcut.register(config.globalShortcuts.volumeUp, () => send('changeVolume', true));
/*
This is used to determine if plugin is actually active
(not if it's only enabled in options)
*/
let isEnabled = false;
export const enabled = () => isEnabled;
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>) => {
isEnabled = true;
injectCSS(win.webContents, volumeHudStyle);
if (options.globalShortcuts?.volumeUp) {
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
} }
if (options.globalShortcuts?.volumeDown) { if (config.globalShortcuts?.volumeDown) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false)); globalShortcut.register(config.globalShortcuts.volumeDown, () => send('changeVolume', false));
} }
}; },
};
});

View File

@ -2,52 +2,26 @@ import prompt, { KeybindOptions } from 'custom-electron-prompt';
import { BrowserWindow, MenuItem } from 'electron'; import { BrowserWindow, MenuItem } from 'electron';
import { enabled } from './main'; import builder, { PreciseVolumePluginConfig } from '.';
import { setMenuOptions } from '../../config/plugins';
import promptOptions from '../../providers/prompt-options'; import promptOptions from '../../providers/prompt-options';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic'; export default builder.createMenu(async ({ setConfig, getConfig, window }) => {
const config = await getConfig();
function changeOptions(changedOptions: Partial<ConfigType<'precise-volume'>>, options: ConfigType<'precise-volume'>, win: BrowserWindow) { function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig, win: BrowserWindow) {
for (const option in changedOptions) { for (const option in changedOptions) {
// HACK: Weird TypeScript error // HACK: Weird TypeScript error
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option]; (options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option];
} }
// Dynamically change setting if plugin is enabled
if (enabled()) { setConfig(options);
win.webContents.send('setOptions', changedOptions);
} else { // Fallback to usual method if disabled
setMenuOptions('precise-volume', options);
} }
}
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>): MenuTemplate => [ // Helper function for globalShortcuts prompt
{ const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
label: 'Local Arrowkeys Controls',
type: 'checkbox',
checked: Boolean(options.arrowsShortcut),
click(item) {
changeOptions({ arrowsShortcut: item.checked }, options, win);
},
},
{
label: 'Global Hotkeys',
type: 'checkbox',
checked: Boolean(options.globalShortcuts?.volumeUp ?? options.globalShortcuts?.volumeDown),
click: (item) => promptGlobalShortcuts(win, options, item),
},
{
label: 'Set Custom Volume Steps',
click: () => promptVolumeSteps(win, options),
},
];
// Helper function for globalShortcuts prompt async function promptVolumeSteps(win: BrowserWindow, options: PreciseVolumePluginConfig) {
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
async function promptVolumeSteps(win: BrowserWindow, options: ConfigType<'precise-volume'>) {
const output = await prompt({ const output = await prompt({
title: 'Volume Steps', title: 'Volume Steps',
label: 'Choose Volume Increase/Decrease Steps', label: 'Choose Volume Increase/Decrease Steps',
@ -61,9 +35,9 @@ async function promptVolumeSteps(win: BrowserWindow, options: ConfigType<'precis
if (output || output === 0) { // 0 is somewhat valid if (output || output === 0) { // 0 is somewhat valid
changeOptions({ steps: output }, options, win); changeOptions({ steps: output }, options, win);
} }
} }
async function promptGlobalShortcuts(win: BrowserWindow, options: ConfigType<'precise-volume'>, item: MenuItem) { async function promptGlobalShortcuts(win: BrowserWindow, options: PreciseVolumePluginConfig, item: MenuItem) {
const output = await prompt({ const output = await prompt({
title: 'Global Volume Keybinds', title: 'Global Volume Keybinds',
label: 'Choose Global Volume Keybinds:', label: 'Choose Global Volume Keybinds:',
@ -91,4 +65,26 @@ async function promptGlobalShortcuts(win: BrowserWindow, options: ConfigType<'pr
// Reset checkbox if prompt was canceled // Reset checkbox if prompt was canceled
item.checked = !item.checked; item.checked = !item.checked;
} }
} }
return [
{
label: 'Local Arrowkeys Controls',
type: 'checkbox',
checked: Boolean(config.arrowsShortcut),
click(item) {
changeOptions({ arrowsShortcut: item.checked }, config, window);
},
},
{
label: 'Global Hotkeys',
type: 'checkbox',
checked: Boolean(config.globalShortcuts?.volumeUp ?? config.globalShortcuts?.volumeDown),
click: (item) => promptGlobalShortcuts(window, config, item),
},
{
label: 'Set Custom Volume Steps',
click: () => promptVolumeSteps(window, config),
},
];
});

View File

@ -1,35 +1,26 @@
import { overrideListener } from './override'; import { overrideListener } from './override';
import builder, { type PreciseVolumePluginConfig } from './';
import { debounce } from '../../providers/decorators'; import { debounce } from '../../providers/decorators';
import type { YoutubePlayer } from '../../types/youtube-player'; import type { YoutubePlayer } from '../../types/youtube-player';
import type { ConfigType } from '../../config/dynamic';
function $<E extends Element = Element>(selector: string) { function $<E extends Element = Element>(selector: string) {
return document.querySelector<E>(selector); return document.querySelector<E>(selector);
} }
let api: YoutubePlayer; let api: YoutubePlayer;
let options: ConfigType<'precise-volume'>;
export default (_options: ConfigType<'precise-volume'>) => { export default builder.createRenderer(async ({ on, getConfig, setConfig }) => {
overrideListener(); let options: PreciseVolumePluginConfig = await getConfig();
options = _options; // Without this function it would rewrite config 20 time when volume change by 20
document.addEventListener('apiLoaded', (e) => { const writeOptions = debounce(() => {
api = e.detail; setConfig(options);
window.ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); }, 1000);
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 moveVolumeHud = debounce((showVideo: boolean) => {
const writeOptions = debounce(() => {
window.mainConfig.plugins.setOptions('precise-volume', options);
}, 1000);
export const moveVolumeHud = debounce((showVideo: boolean) => {
const volumeHud = $<HTMLElement>('#volumeHud'); const volumeHud = $<HTMLElement>('#volumeHud');
if (!volumeHud) { if (!volumeHud) {
return; return;
@ -38,18 +29,18 @@ export const moveVolumeHud = debounce((showVideo: boolean) => {
volumeHud.style.top = showVideo volumeHud.style.top = showVideo
? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px` ? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px`
: '0'; : '0';
}, 250); }, 250);
const hideVolumeHud = debounce((volumeHud: HTMLElement) => { const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
volumeHud.style.opacity = '0'; volumeHud.style.opacity = '0';
}, 2000); }, 2000);
const hideVolumeSlider = debounce((slider: HTMLElement) => { const hideVolumeSlider = debounce((slider: HTMLElement) => {
slider.classList.remove('on-hover'); slider.classList.remove('on-hover');
}, 2500); }, 2500);
/** Restore saved volume and setup tooltip */ /** Restore saved volume and setup tooltip */
function firstRun() { function firstRun() {
if (typeof options.savedVolume === 'number') { if (typeof options.savedVolume === 'number') {
// Set saved volume as tooltip // Set saved volume as tooltip
setTooltip(options.savedVolume); setTooltip(options.savedVolume);
@ -74,15 +65,9 @@ function firstRun() {
$('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode())); $('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode()));
} }
} }
}
// Change options from renderer to keep sync function injectVolumeHud(noVid: boolean) {
window.ipcRenderer.on('setOptions', (_event, newOptions = {}) => {
Object.assign(options, newOptions);
window.mainConfig.plugins.setMenuOptions('precise-volume', options);
});
}
function injectVolumeHud(noVid: boolean) {
if (noVid) { if (noVid) {
const position = 'top: 18px; right: 60px;'; const position = 'top: 18px; right: 60px;';
const mainStyle = 'font-size: xx-large;'; const mainStyle = 'font-size: xx-large;';
@ -100,9 +85,9 @@ function injectVolumeHud(noVid: boolean) {
`<span id="volumeHud" style="${position + mainStyle}"></span>`, `<span id="volumeHud" style="${position + mainStyle}"></span>`,
); );
} }
} }
function showVolumeHud(volume: number) { function showVolumeHud(volume: number) {
const volumeHud = $<HTMLElement>('#volumeHud'); const volumeHud = $<HTMLElement>('#volumeHud');
if (!volumeHud) { if (!volumeHud) {
return; return;
@ -112,10 +97,10 @@ function showVolumeHud(volume: number) {
volumeHud.style.opacity = '1'; volumeHud.style.opacity = '1';
hideVolumeHud(volumeHud); hideVolumeHud(volumeHud);
} }
/** Add onwheel event to video player */ /** Add onwheel event to video player */
function setupVideoPlayerOnwheel() { function setupVideoPlayerOnwheel() {
const panel = $<HTMLElement>('#main-panel'); const panel = $<HTMLElement>('#main-panel');
if (!panel) return; if (!panel) return;
@ -124,15 +109,15 @@ function setupVideoPlayerOnwheel() {
// Event.deltaY < 0 means wheel-up // Event.deltaY < 0 means wheel-up
changeVolume(event.deltaY < 0); changeVolume(event.deltaY < 0);
}); });
} }
function saveVolume(volume: number) { function saveVolume(volume: number) {
options.savedVolume = volume; options.savedVolume = volume;
writeOptions(); writeOptions();
} }
/** Add onwheel event to play bar and also track if play bar is hovered */ /** Add onwheel event to play bar and also track if play bar is hovered */
function setupPlaybar() { function setupPlaybar() {
const playerbar = $<HTMLElement>('ytmusic-player-bar'); const playerbar = $<HTMLElement>('ytmusic-player-bar');
if (!playerbar) return; if (!playerbar) return;
@ -152,10 +137,10 @@ function setupPlaybar() {
}); });
setupSliderObserver(); setupSliderObserver();
} }
/** Save volume + Update the volume tooltip when volume-slider is manually changed */ /** Save volume + Update the volume tooltip when volume-slider is manually changed */
function setupSliderObserver() { function setupSliderObserver() {
const sliderObserver = new MutationObserver((mutations) => { const sliderObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) { for (const mutation of mutations) {
if (mutation.target instanceof HTMLInputElement) { if (mutation.target instanceof HTMLInputElement) {
@ -180,9 +165,9 @@ function setupSliderObserver() {
attributeFilter: ['value'], attributeFilter: ['value'],
attributeOldValue: true, attributeOldValue: true,
}); });
} }
function setVolume(value: number) { function setVolume(value: number) {
api.setVolume(value); api.setVolume(value);
// Save the new volume // Save the new volume
saveVolume(value); saveVolume(value);
@ -196,18 +181,18 @@ function setVolume(value: number) {
showVolumeSlider(); showVolumeSlider();
// Show volume HUD // Show volume HUD
showVolumeHud(value); showVolumeHud(value);
} }
/** If (toIncrease = false) then volume decrease */ /** If (toIncrease = false) then volume decrease */
function changeVolume(toIncrease: boolean) { function changeVolume(toIncrease: boolean) {
// Apply volume change if valid // Apply volume change if valid
const steps = Number(options.steps || 1); const steps = Number(options.steps || 1);
setVolume(toIncrease setVolume(toIncrease
? Math.min(api.getVolume() + steps, 100) ? Math.min(api.getVolume() + steps, 100)
: Math.max(api.getVolume() - steps, 0)); : Math.max(api.getVolume() - steps, 0));
} }
function updateVolumeSlider() { function updateVolumeSlider() {
const savedVolume = options.savedVolume ?? 0; const savedVolume = options.savedVolume ?? 0;
// Slider value automatically rounds to multiples of 5 // Slider value automatically rounds to multiples of 5
for (const slider of ['#volume-slider', '#expand-volume-slider']) { for (const slider of ['#volume-slider', '#expand-volume-slider']) {
@ -216,9 +201,9 @@ function updateVolumeSlider() {
silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume); silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume);
} }
} }
} }
function showVolumeSlider() { function showVolumeSlider() {
const slider = $<HTMLElement>('#volume-slider'); const slider = $<HTMLElement>('#volume-slider');
if (!slider) return; if (!slider) return;
@ -226,26 +211,26 @@ function showVolumeSlider() {
slider.classList.add('on-hover'); slider.classList.add('on-hover');
hideVolumeSlider(slider); hideVolumeSlider(slider);
} }
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small) // Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
const tooltipTargets = [ const tooltipTargets = [
'#volume-slider', '#volume-slider',
'tp-yt-paper-icon-button.volume', 'tp-yt-paper-icon-button.volume',
'#expand-volume-slider', '#expand-volume-slider',
'#expand-volume', '#expand-volume',
]; ];
function setTooltip(volume: number) { function setTooltip(volume: number) {
for (const target of tooltipTargets) { for (const target of tooltipTargets) {
const tooltipTargetElement = $<HTMLElement>(target); const tooltipTargetElement = $<HTMLElement>(target);
if (tooltipTargetElement) { if (tooltipTargetElement) {
tooltipTargetElement.title = `${volume}%`; tooltipTargetElement.title = `${volume}%`;
} }
} }
} }
function setupLocalArrowShortcuts() { function setupLocalArrowShortcuts() {
if (options.arrowsShortcut) { if (options.arrowsShortcut) {
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) { if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) {
@ -267,4 +252,22 @@ function setupLocalArrowShortcuts() {
} }
}); });
} }
} }
return {
onLoad() {
overrideListener();
document.addEventListener('apiLoaded', (e) => {
api = e.detail;
on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease));
on('setVolume', (_, value: number) => setVolume(value));
firstRun();
}, { once: true, passive: true });
},
onConfigChange(config) {
options = config;
}
};
});

View File

@ -43,7 +43,6 @@ export default builder.createRenderer(({ invoke }) => {
return { return {
onLoad() { onLoad() {
console.log('qc');
document.addEventListener('apiLoaded', setup, { once: true, passive: true }); document.addEventListener('apiLoaded', setup, { once: true, passive: true });
} }
}; };

View File

@ -35,11 +35,14 @@ export type RendererPluginContext<Config extends PluginBaseConfig = PluginBaseCo
invoke: <Return>(event: string, ...args: unknown[]) => Promise<Return>; invoke: <Return>(event: string, ...args: unknown[]) => Promise<Return>;
on: <Arguments extends unknown[]>(event: string, listener: (...args: Arguments) => Promisable<void>) => void; on: <Arguments extends unknown[]>(event: string, listener: (...args: Arguments) => Promisable<void>) => void;
}; };
export type MenuPluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = PluginContext<Config> & {
window: BrowserWindow;
};
export type RendererPluginFactory<Config extends PluginBaseConfig> = (context: RendererPluginContext<Config>) => Promisable<RendererPlugin<Config>>; export type RendererPluginFactory<Config extends PluginBaseConfig> = (context: RendererPluginContext<Config>) => Promisable<RendererPlugin<Config>>;
export type MainPluginFactory<Config extends PluginBaseConfig> = (context: MainPluginContext<Config>) => Promisable<MainPlugin<Config>>; export type MainPluginFactory<Config extends PluginBaseConfig> = (context: MainPluginContext<Config>) => Promisable<MainPlugin<Config>>;
export type PreloadPluginFactory<Config extends PluginBaseConfig> = (context: PluginContext<Config>) => Promisable<PreloadPlugin<Config>>; export type PreloadPluginFactory<Config extends PluginBaseConfig> = (context: PluginContext<Config>) => Promisable<PreloadPlugin<Config>>;
export type MenuPluginFactory<Config extends PluginBaseConfig> = (context: PluginContext<Config>) => Promisable<MenuItemConstructorOptions[]>; export type MenuPluginFactory<Config extends PluginBaseConfig> = (context: MenuPluginContext<Config>) => Promisable<MenuItemConstructorOptions[]>;
export type PluginBuilder<ID extends string, Config extends PluginBaseConfig> = { export type PluginBuilder<ID extends string, Config extends PluginBaseConfig> = {
createRenderer: IF<RendererPluginFactory<Config>>; createRenderer: IF<RendererPluginFactory<Config>>;

View File

@ -7,7 +7,6 @@ export const injectCSS = (webContents: Electron.WebContents, css: string, cb: ((
setupCssInjection(webContents); setupCssInjection(webContents);
} }
console.log('injectCSS', css);
cssToInject.set(css, cb); cssToInject.set(css, cb);
}; };

View File

@ -2,14 +2,15 @@ import buttonTemplate from './templates/button_template.html?raw';
import { ElementFromHtml } from '../utils/renderer'; import { ElementFromHtml } from '../utils/renderer';
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/renderer'; // import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/renderer';
import { YoutubePlayer } from '../../types/youtube-player'; import { YoutubePlayer } from '../../types/youtube-player';
import { ThumbnailElement } from '../../types/get-player-response'; import { ThumbnailElement } from '../../types/get-player-response';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; // const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {};
const moveVolumeHud = () => {};
function $<E extends Element = Element>(selector: string): E | null { function $<E extends Element = Element>(selector: string): E | null {
return document.querySelector<E>(selector); return document.querySelector<E>(selector);

View File

@ -2,7 +2,7 @@
// eslint-disable-next-line import/order // eslint-disable-next-line import/order
import { rendererPlugins } from 'virtual:RendererPlugins'; import { rendererPlugins } from 'virtual:RendererPlugins';
import { PluginBaseConfig, RendererPluginContext } from './plugins/utils/builder'; import { PluginBaseConfig, RendererPluginContext, RendererPluginFactory } from './plugins/utils/builder';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import { setupSongControls } from './providers/song-controls-front'; import { setupSongControls } from './providers/song-controls-front';
@ -99,7 +99,9 @@ const createContext = <
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'], Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
>(name: Key): RendererPluginContext<Config> => ({ >(name: Key): RendererPluginContext<Config> => ({
getConfig: async () => { getConfig: async () => {
return await window.ipcRenderer.invoke('get-config', name) as Config; const result = await window.ipcRenderer.invoke('get-config', name) as Config;
return result;
}, },
setConfig: async (newConfig) => { setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('set-config', name, newConfig); await window.ipcRenderer.invoke('set-config', name, newConfig);
@ -114,39 +116,62 @@ const createContext = <
}); });
(async () => { (async () => {
enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => { // enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => {
if (pluginName === 'ambient-mode') { // if (pluginName === 'ambient-mode') {
const builder = rendererPlugins[pluginName]; // const builder = rendererPlugins[pluginName];
try { // try {
const context = createContext(pluginName); // const context = createContext(pluginName);
const plugin = await builder?.(context); // const plugin = await builder?.(context);
console.log(plugin); // console.log(plugin);
plugin.onLoad?.(); // plugin.onLoad?.();
} catch (error) { // } catch (error) {
console.error(`Error in plugin "${pluginName}"`); // console.error(`Error in plugin "${pluginName}"`);
console.trace(error); // console.trace(error);
} // }
} // }
if (Object.hasOwn(rendererPlugins, pluginName)) { // if (Object.hasOwn(rendererPlugins, pluginName)) {
const handler = rendererPlugins[pluginName]; // const handler = rendererPlugins[pluginName];
try { // try {
await handler?.(options as never); // await handler?.(options as never);
} catch (error) { // } catch (error) {
console.error(`Error in plugin "${pluginName}"`); // console.error(`Error in plugin "${pluginName}"`);
console.trace(error); // console.trace(error);
} // }
} // }
}); // });
const rendererPluginList = await Promise.all(
newPluginList.map(async ([id, plugin]) => { const rendererPluginResult = await Promise.allSettled(
const context = createContext(id); enabledPluginNameAndOptions.map(async ([id]) => {
return [id, await plugin(context)] as const; const builder = (rendererPlugins as Record<string, RendererPluginFactory<PluginBaseConfig>>)[id];
const context = createContext(id as never);
return [id, await builder(context as never)] as const;
}), }),
); );
rendererPluginList.forEach(([, plugin]) => plugin.onLoad?.()); const rendererPluginList = rendererPluginResult
.map((it) => it.status === 'fulfilled' ? it.value : null)
.filter(Boolean);
rendererPluginResult.forEach((it, index) => {
if (it.status === 'rejected') {
const id = enabledPluginNameAndOptions[index][0];
console.error('[YTMusic]', `Cannot load plugin "${id}"`);
console.trace(it.reason);
}
});
rendererPluginList.forEach(([id, plugin]) => {
try {
plugin.onLoad?.();
console.log('[YTMusic]', `"${id}" plugin is loaded`);
} catch (error) {
console.error('[YTMusic]', `Cannot load plugin "${id}"`);
console.trace(error);
}
});
window.ipcRenderer.on('config-changed', (_event, id: string, newConfig) => { window.ipcRenderer.on('config-changed', (_event, id: string, newConfig) => {
const plugin = rendererPluginList.find(([pluginId]) => pluginId === id); const plugin = rendererPluginList.find(([pluginId]) => pluginId === id);

View File

@ -1,24 +1,27 @@
declare module 'virtual:MainPlugins' { declare module 'virtual:MainPlugins' {
import type { MainPluginFactory } from './plugins/utils/builder'; import type { MainPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
export const mainPlugins: Record<string, MainPluginFactory>; export const mainPlugins: Record<string, MainPluginFactory<PluginBaseConfig>>;
} }
declare module 'virtual:MenuPlugins' { declare module 'virtual:MenuPlugins' {
import type { MenuPluginFactory } from './plugins/utils/builder'; import type { MenuPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
export const menuPlugins: Record<string, MenuPluginFactory>; export const menuPlugins: Record<string, MenuPluginFactory<PluginBaseConfig>>;
} }
declare module 'virtual:PreloadPlugins' { declare module 'virtual:PreloadPlugins' {
import type { PreloadPluginFactory } from './plugins/utils/builder'; import type { PreloadPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
export const preloadPlugins: Record<string, PreloadPluginFactory>; export const preloadPlugins: Record<string, PreloadPluginFactory<PluginBaseConfig>>;
} }
declare module 'virtual:RendererPlugins' { declare module 'virtual:RendererPlugins' {
import type { RendererPluginFactory } from './plugins/utils/builder'; import type { RendererPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
export const rendererPlugins: Record<string, RendererPluginFactory>; export const rendererPlugins: Record<string, RendererPluginFactory<PluginBaseConfig>>;
}
declare module 'virtual:PluginBuilders' {
export const pluginBuilders: PluginBuilderList;
} }

View File

@ -3,31 +3,45 @@ import { basename, relative, resolve } from 'node:path';
import { globSync } from 'glob'; import { globSync } from 'glob';
const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); type PluginType = 'index' | 'main' | 'preload' | 'renderer' | 'menu';
export const pluginVirtualModuleGenerator = (mode: 'main' | 'preload' | 'renderer' | 'menu') => { const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
const getName = (mode: PluginType, name: string) => {
if (mode === 'index') {
return snakeToCamel(name);
}
return `${snakeToCamel(name)}Plugin`;
};
const getListName = (mode: PluginType) => {
if (mode === 'index') return 'pluginBuilders';
return `${mode}Plugins`;
};
export const pluginVirtualModuleGenerator = (mode: PluginType) => {
const srcPath = resolve(__dirname, '..', 'src'); const srcPath = resolve(__dirname, '..', 'src');
const plugins = globSync(`${srcPath}/plugins/*`) const plugins = globSync(`${srcPath}/plugins/*`)
.map((path) => ({ name: basename(path), path })) .map((path) => ({ name: basename(path), path }))
.filter(({ name, path }) => { .filter(({ name, path }) => {
if (name.startsWith('utils')) return false; if (name.startsWith('utils')) return false;
if (path.includes('ambient-mode')) return false;
if (path.includes('quality')) return false;
return existsSync(resolve(path, `${mode}.ts`)); return existsSync(resolve(path, `${mode}.ts`));
}); });
// for test !name.startsWith('ambient-mode')
console.log('converted plugin list');
console.log(plugins.map((it) => it.name));
let result = ''; let result = '';
for (const { name, path } of plugins) { for (const { name, path } of plugins) {
result += `import ${snakeToCamel(name)}Plugin from "./${relative(resolve(srcPath, '..'), path).replace(/\\/g, '/')}/${mode}";\n`; result += `import ${getName(mode, name)} from "./${relative(resolve(srcPath, '..'), path).replace(/\\/g, '/')}/${mode}";\n`;
} }
result += `export const ${mode}Plugins = {\n`; result += `export const ${getListName(mode)} = {\n`;
for (const { name } of plugins) { for (const { name } of plugins) {
result += ` "${name}": ${snakeToCamel(name)}Plugin,\n`; result += ` "${name}": ${getName(mode, name)},\n`;
} }
result += '};'; result += '};';