mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 12:21:47 +00:00
feat(plugin): migrate some plugin (WIP)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -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'),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
76
src/index.ts
76
src/index.ts
@ -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`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (factory) {
|
||||||
if (Object.hasOwn(mainPlugins, plugin)) {
|
try {
|
||||||
console.log('Loaded plugin - ' + plugin);
|
const context = createContext(pluginId as keyof PluginBuilderList);
|
||||||
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
|
const plugin = await factory(context);
|
||||||
if (handler) {
|
loadedPluginList.push([pluginId, plugin]);
|
||||||
await handler(win, options as never);
|
plugin.onLoad?.(win);
|
||||||
}
|
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);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/menu.ts
60
src/menu.ts
@ -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',
|
||||||
|
|||||||
16
src/plugins/audio-compressor/index.ts
Normal file
16
src/plugins/audio-compressor/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,24 @@
|
|||||||
export default () =>
|
import builder from '.';
|
||||||
document.addEventListener('audioCanPlay', (e) => {
|
|
||||||
const { audioContext } = e.detail;
|
|
||||||
|
|
||||||
const compressor = audioContext.createDynamicsCompressor();
|
export default builder.createRenderer(() => {
|
||||||
compressor.threshold.value = -50;
|
return {
|
||||||
compressor.ratio.value = 12;
|
onLoad() {
|
||||||
compressor.knee.value = 40;
|
document.addEventListener('audioCanPlay', (e) => {
|
||||||
compressor.attack.value = 0;
|
const { audioContext } = e.detail;
|
||||||
compressor.release.value = 0.25;
|
|
||||||
|
|
||||||
e.detail.audioSource.connect(compressor);
|
const compressor = audioContext.createDynamicsCompressor();
|
||||||
compressor.connect(audioContext.destination);
|
compressor.threshold.value = -50;
|
||||||
}, {
|
compressor.ratio.value = 12;
|
||||||
once: true, // Only create the audio compressor once, not on each video
|
compressor.knee.value = 40;
|
||||||
passive: true,
|
compressor.attack.value = 0;
|
||||||
});
|
compressor.release.value = 0.25;
|
||||||
|
|
||||||
|
e.detail.audioSource.connect(compressor);
|
||||||
|
compressor.connect(audioContext.destination);
|
||||||
|
}, {
|
||||||
|
once: true, // Only create the audio compressor once, not on each video
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
19
src/plugins/blur-nav-bar/index.ts
Normal file
19
src/plugins/blur-nav-bar/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
};
|
|
||||||
16
src/plugins/bypass-age-restrictions/index.ts
Normal file
16
src/plugins/bypass-age-restrictions/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
export default async () => {
|
import builder from '.';
|
||||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
|
||||||
await import('simple-youtube-age-restriction-bypass');
|
export default builder.createRenderer(() => ({
|
||||||
};
|
async onLoad() {
|
||||||
|
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||||
|
await import('simple-youtube-age-restriction-bypass');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
20
src/plugins/in-app-menu/index.ts
Normal file
20
src/plugins/in-app-menu/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,75 +2,75 @@ 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
|
return {
|
||||||
export default (win: BrowserWindow) => {
|
onLoad(win) {
|
||||||
injectCSS(win.webContents, titlebarStyle);
|
win.on('close', () => {
|
||||||
|
win.webContents.send('close-all-in-app-menu-panel');
|
||||||
|
});
|
||||||
|
|
||||||
win.on('close', () => {
|
win.once('ready-to-show', () => {
|
||||||
win.webContents.send('close-all-in-app-menu-panel');
|
register(win, '`', () => {
|
||||||
});
|
win.webContents.send('toggle-in-app-menu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
handle(
|
||||||
register(win, '`', () => {
|
'get-menu',
|
||||||
win.webContents.send('toggle-in-app-menu');
|
() => JSON.parse(JSON.stringify(
|
||||||
});
|
Menu.getApplicationMenu(),
|
||||||
});
|
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
const getMenuItemById = (commandId: number): MenuItem | null => {
|
||||||
'get-menu',
|
const menu = Menu.getApplicationMenu();
|
||||||
() => JSON.parse(JSON.stringify(
|
|
||||||
Menu.getApplicationMenu(),
|
|
||||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMenuItemById = (commandId: number): MenuItem | null => {
|
let target: MenuItem | null = null;
|
||||||
const menu = Menu.getApplicationMenu();
|
const stack = [...menu?.items ?? []];
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const now = stack.shift();
|
||||||
|
now?.submenu?.items.forEach((item) => stack.push(item));
|
||||||
|
|
||||||
let target: MenuItem | null = null;
|
if (now?.commandId === commandId) {
|
||||||
const stack = [...menu?.items ?? []];
|
target = now;
|
||||||
while (stack.length > 0) {
|
break;
|
||||||
const now = stack.shift();
|
}
|
||||||
now?.submenu?.items.forEach((item) => stack.push(item));
|
}
|
||||||
|
|
||||||
if (now?.commandId === commandId) {
|
return target;
|
||||||
target = now;
|
};
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
ipcMain.handle('menu-event', (event, commandId: number) => {
|
||||||
|
const target = getMenuItemById(commandId);
|
||||||
|
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle('get-menu-by-id', (_, commandId: number) => {
|
||||||
|
const result = getMenuItemById(commandId);
|
||||||
|
|
||||||
|
return JSON.parse(JSON.stringify(
|
||||||
|
result,
|
||||||
|
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle('window-is-maximized', () => win.isMaximized());
|
||||||
|
|
||||||
|
handle('window-close', () => win.close());
|
||||||
|
handle('window-minimize', () => win.minimize());
|
||||||
|
handle('window-maximize', () => win.maximize());
|
||||||
|
win.on('maximize', () => win.webContents.send('window-maximize'));
|
||||||
|
handle('window-unmaximize', () => win.unmaximize());
|
||||||
|
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
|
||||||
|
|
||||||
|
handle('image-path-to-data-url', (_, imagePath: string) => {
|
||||||
|
const nativeImageIcon = nativeImage.createFromPath(imagePath);
|
||||||
|
return nativeImageIcon?.toDataURL();
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
});
|
||||||
ipcMain.handle('menu-event', (event, commandId: number) => {
|
|
||||||
const target = getMenuItemById(commandId);
|
|
||||||
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-menu-by-id', (_, commandId: number) => {
|
|
||||||
const result = getMenuItemById(commandId);
|
|
||||||
|
|
||||||
return JSON.parse(JSON.stringify(
|
|
||||||
result,
|
|
||||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('window-is-maximized', () => win.isMaximized());
|
|
||||||
|
|
||||||
ipcMain.handle('window-close', () => win.close());
|
|
||||||
ipcMain.handle('window-minimize', () => win.minimize());
|
|
||||||
ipcMain.handle('window-maximize', () => win.maximize());
|
|
||||||
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,22 +1,25 @@
|
|||||||
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',
|
||||||
checked: config.hideDOMWindowControls,
|
checked: config.hideDOMWindowControls,
|
||||||
click(item) {
|
click(item) {
|
||||||
config.hideDOMWindowControls = item.checked;
|
config.hideDOMWindowControls = item.checked;
|
||||||
setMenuOptions('in-app-menu', config);
|
setMenuOptions('in-app-menu', config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
];
|
||||||
] : []) satisfies Electron.MenuItemConstructorOptions[],
|
}
|
||||||
];
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|||||||
@ -6,186 +6,191 @@ 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 {
|
||||||
let hideMenu = window.mainConfig.get('options.hideMenu');
|
async onLoad() {
|
||||||
const titleBar = document.createElement('title-bar');
|
const config = await getConfig();
|
||||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
|
||||||
let maximizeButton: HTMLButtonElement;
|
|
||||||
let panelClosers: (() => void)[] = [];
|
|
||||||
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
|
||||||
|
|
||||||
const logo = document.createElement('img');
|
const hideDOMWindowControls = config.hideDOMWindowControls;
|
||||||
const close = document.createElement('img');
|
|
||||||
const minimize = document.createElement('img');
|
|
||||||
const maximize = document.createElement('img');
|
|
||||||
const unmaximize = document.createElement('img');
|
|
||||||
|
|
||||||
if (window.ELECTRON_RENDERER_URL) {
|
let hideMenu = window.mainConfig.get('options.hideMenu');
|
||||||
logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw;
|
const titleBar = document.createElement('title-bar');
|
||||||
close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw;
|
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||||
minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw;
|
let maximizeButton: HTMLButtonElement;
|
||||||
maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw;
|
let panelClosers: (() => void)[] = [];
|
||||||
unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw;
|
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
||||||
} else {
|
|
||||||
logo.src = logoRaw;
|
|
||||||
close.src = closeRaw;
|
|
||||||
minimize.src = minimizeRaw;
|
|
||||||
maximize.src = maximizeRaw;
|
|
||||||
unmaximize.src = unmaximizeRaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
logo.classList.add('title-bar-icon');
|
const logo = document.createElement('img');
|
||||||
const logoClick = () => {
|
const close = document.createElement('img');
|
||||||
hideMenu = !hideMenu;
|
const minimize = document.createElement('img');
|
||||||
let visibilityStyle: string;
|
const maximize = document.createElement('img');
|
||||||
if (hideMenu) {
|
const unmaximize = document.createElement('img');
|
||||||
visibilityStyle = 'hidden';
|
|
||||||
} else {
|
|
||||||
visibilityStyle = 'visible';
|
|
||||||
}
|
|
||||||
const menus = document.querySelectorAll<HTMLElement>('menu-button');
|
|
||||||
menus.forEach((menu) => {
|
|
||||||
menu.style.visibility = visibilityStyle;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
logo.onclick = logoClick;
|
|
||||||
|
|
||||||
window.ipcRenderer.on('toggle-in-app-menu', logoClick);
|
if (window.ELECTRON_RENDERER_URL) {
|
||||||
|
logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw;
|
||||||
if (!isMacOS) titleBar.appendChild(logo);
|
close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw;
|
||||||
document.body.appendChild(titleBar);
|
minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw;
|
||||||
|
maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw;
|
||||||
titleBar.appendChild(logo);
|
unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw;
|
||||||
|
|
||||||
const addWindowControls = async () => {
|
|
||||||
|
|
||||||
// Create window control buttons
|
|
||||||
const minimizeButton = document.createElement('button');
|
|
||||||
minimizeButton.classList.add('window-control');
|
|
||||||
minimizeButton.appendChild(minimize);
|
|
||||||
minimizeButton.onclick = () => window.ipcRenderer.invoke('window-minimize');
|
|
||||||
|
|
||||||
maximizeButton = document.createElement('button');
|
|
||||||
if (await window.ipcRenderer.invoke('window-is-maximized')) {
|
|
||||||
maximizeButton.classList.add('window-control');
|
|
||||||
maximizeButton.appendChild(unmaximize);
|
|
||||||
} else {
|
|
||||||
maximizeButton.classList.add('window-control');
|
|
||||||
maximizeButton.appendChild(maximize);
|
|
||||||
}
|
|
||||||
maximizeButton.onclick = async () => {
|
|
||||||
if (await window.ipcRenderer.invoke('window-is-maximized')) {
|
|
||||||
// change icon to maximize
|
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
|
||||||
maximizeButton.appendChild(maximize);
|
|
||||||
|
|
||||||
// call unmaximize
|
|
||||||
await window.ipcRenderer.invoke('window-unmaximize');
|
|
||||||
} else {
|
} else {
|
||||||
// change icon to unmaximize
|
logo.src = logoRaw;
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
close.src = closeRaw;
|
||||||
maximizeButton.appendChild(unmaximize);
|
minimize.src = minimizeRaw;
|
||||||
|
maximize.src = maximizeRaw;
|
||||||
// call maximize
|
unmaximize.src = unmaximizeRaw;
|
||||||
await window.ipcRenderer.invoke('window-maximize');
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const closeButton = document.createElement('button');
|
logo.classList.add('title-bar-icon');
|
||||||
closeButton.classList.add('window-control');
|
const logoClick = () => {
|
||||||
closeButton.appendChild(close);
|
hideMenu = !hideMenu;
|
||||||
closeButton.onclick = () => window.ipcRenderer.invoke('window-close');
|
let visibilityStyle: string;
|
||||||
|
if (hideMenu) {
|
||||||
|
visibilityStyle = 'hidden';
|
||||||
|
} else {
|
||||||
|
visibilityStyle = 'visible';
|
||||||
|
}
|
||||||
|
const menus = document.querySelectorAll<HTMLElement>('menu-button');
|
||||||
|
menus.forEach((menu) => {
|
||||||
|
menu.style.visibility = visibilityStyle;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
logo.onclick = logoClick;
|
||||||
|
|
||||||
// Create a container div for the window control buttons
|
on('toggle-in-app-menu', logoClick);
|
||||||
const windowControlsContainer = document.createElement('div');
|
|
||||||
windowControlsContainer.classList.add('window-controls-container');
|
|
||||||
windowControlsContainer.appendChild(minimizeButton);
|
|
||||||
windowControlsContainer.appendChild(maximizeButton);
|
|
||||||
windowControlsContainer.appendChild(closeButton);
|
|
||||||
|
|
||||||
// Add window control buttons to the title bar
|
if (!isMacOS) titleBar.appendChild(logo);
|
||||||
titleBar.appendChild(windowControlsContainer);
|
document.body.appendChild(titleBar);
|
||||||
};
|
|
||||||
|
|
||||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
titleBar.appendChild(logo);
|
||||||
|
|
||||||
if (navBar) {
|
const addWindowControls = async () => {
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach(() => {
|
// Create window control buttons
|
||||||
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
const minimizeButton = document.createElement('button');
|
||||||
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
minimizeButton.classList.add('window-control');
|
||||||
|
minimizeButton.appendChild(minimize);
|
||||||
|
minimizeButton.onclick = () => invoke('window-minimize');
|
||||||
|
|
||||||
|
maximizeButton = document.createElement('button');
|
||||||
|
if (await invoke('window-is-maximized')) {
|
||||||
|
maximizeButton.classList.add('window-control');
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
} else {
|
||||||
|
maximizeButton.classList.add('window-control');
|
||||||
|
maximizeButton.appendChild(maximize);
|
||||||
|
}
|
||||||
|
maximizeButton.onclick = async () => {
|
||||||
|
if (await invoke('window-is-maximized')) {
|
||||||
|
// change icon to maximize
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||||
|
maximizeButton.appendChild(maximize);
|
||||||
|
|
||||||
|
// call unmaximize
|
||||||
|
await invoke('window-unmaximize');
|
||||||
|
} else {
|
||||||
|
// change icon to unmaximize
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
|
||||||
|
// call maximize
|
||||||
|
await invoke('window-maximize');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeButton = document.createElement('button');
|
||||||
|
closeButton.classList.add('window-control');
|
||||||
|
closeButton.appendChild(close);
|
||||||
|
closeButton.onclick = () => invoke('window-close');
|
||||||
|
|
||||||
|
// Create a container div for the window control buttons
|
||||||
|
const windowControlsContainer = document.createElement('div');
|
||||||
|
windowControlsContainer.classList.add('window-controls-container');
|
||||||
|
windowControlsContainer.appendChild(minimizeButton);
|
||||||
|
windowControlsContainer.appendChild(maximizeButton);
|
||||||
|
windowControlsContainer.appendChild(closeButton);
|
||||||
|
|
||||||
|
// Add window control buttons to the title bar
|
||||||
|
titleBar.appendChild(windowControlsContainer);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||||
|
|
||||||
|
if (navBar) {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach(() => {
|
||||||
|
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||||
|
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(navBar, { attributes : true, attributeFilter : ['style'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMenu = async () => {
|
||||||
|
const children = [...titleBar.children];
|
||||||
|
children.forEach((child) => {
|
||||||
|
if (child !== logo) child.remove();
|
||||||
|
});
|
||||||
|
panelClosers = [];
|
||||||
|
|
||||||
|
const menu = await invoke<Menu | null>('get-menu');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
menu.items.forEach((menuItem) => {
|
||||||
|
const menu = document.createElement('menu-button');
|
||||||
|
const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []);
|
||||||
|
panelClosers.push(closer);
|
||||||
|
|
||||||
|
menu.append(menuItem.label);
|
||||||
|
titleBar.appendChild(menu);
|
||||||
|
if (hideMenu) {
|
||||||
|
menu.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||||
|
};
|
||||||
|
await updateMenu();
|
||||||
|
|
||||||
|
document.title = 'Youtube Music';
|
||||||
|
|
||||||
|
on('close-all-in-app-menu-panel', () => {
|
||||||
|
panelClosers.forEach((closer) => closer());
|
||||||
|
});
|
||||||
|
on('refresh-in-app-menu', () => updateMenu());
|
||||||
|
on('window-maximize', () => {
|
||||||
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
on('window-unmaximize', () => {
|
||||||
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||||
|
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(navBar, { attributes : true, attributeFilter : ['style'] });
|
if (window.mainConfig.plugins.isEnabled('picture-in-picture')) {
|
||||||
}
|
on('pip-toggle', () => {
|
||||||
|
updateMenu();
|
||||||
const updateMenu = async () => {
|
});
|
||||||
const children = [...titleBar.children];
|
|
||||||
children.forEach((child) => {
|
|
||||||
if (child !== logo) child.remove();
|
|
||||||
});
|
|
||||||
panelClosers = [];
|
|
||||||
|
|
||||||
const menu = await window.ipcRenderer.invoke('get-menu') as Menu | null;
|
|
||||||
if (!menu) return;
|
|
||||||
|
|
||||||
menu.items.forEach((menuItem) => {
|
|
||||||
const menu = document.createElement('menu-button');
|
|
||||||
const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []);
|
|
||||||
panelClosers.push(closer);
|
|
||||||
|
|
||||||
menu.append(menuItem.label);
|
|
||||||
titleBar.appendChild(menu);
|
|
||||||
if (hideMenu) {
|
|
||||||
menu.style.visibility = 'hidden';
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
// 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', () => {
|
||||||
|
const htmlHeadStyle = document.querySelector('head > div > style');
|
||||||
|
if (htmlHeadStyle) {
|
||||||
|
// 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 {');
|
||||||
|
}
|
||||||
|
}, { once: true, passive: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
await updateMenu();
|
});
|
||||||
|
|
||||||
document.title = 'Youtube Music';
|
|
||||||
|
|
||||||
window.ipcRenderer.on('close-all-in-app-menu-panel', () => {
|
|
||||||
panelClosers.forEach((closer) => closer());
|
|
||||||
});
|
|
||||||
window.ipcRenderer.on('refresh-in-app-menu', () => updateMenu());
|
|
||||||
window.ipcRenderer.on('window-maximize', () => {
|
|
||||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
|
||||||
maximizeButton.appendChild(unmaximize);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.ipcRenderer.on('window-unmaximize', () => {
|
|
||||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
|
||||||
maximizeButton.appendChild(unmaximize);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.mainConfig.plugins.isEnabled('picture-in-picture')) {
|
|
||||||
window.ipcRenderer.on('pip-toggle', () => {
|
|
||||||
updateMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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', () => {
|
|
||||||
const htmlHeadStyle = $('head > div > style');
|
|
||||||
if (htmlHeadStyle) {
|
|
||||||
// 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 {');
|
|
||||||
}
|
|
||||||
}, { once: true, passive: true });
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
19
src/plugins/navigation/index.ts
Normal file
19
src/plugins/navigation/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
20
src/plugins/navigation/renderer.ts
Normal file
20
src/plugins/navigation/renderer.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
37
src/plugins/precise-volume/index.ts
Normal file
37
src/plugins/precise-volume/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
if (config.globalShortcuts?.volumeDown) {
|
||||||
This is used to determine if plugin is actually active
|
globalShortcut.register(config.globalShortcuts.volumeDown, () => send('changeVolume', false));
|
||||||
(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) {
|
|
||||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@ -2,93 +2,89 @@ 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()) {
|
|
||||||
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 => [
|
|
||||||
{
|
|
||||||
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
|
|
||||||
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({
|
|
||||||
title: 'Volume Steps',
|
|
||||||
label: 'Choose Volume Increase/Decrease Steps',
|
|
||||||
value: options.steps || 1,
|
|
||||||
type: 'counter',
|
|
||||||
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
|
||||||
width: 380,
|
|
||||||
...promptOptions(),
|
|
||||||
}, win);
|
|
||||||
|
|
||||||
if (output || output === 0) { // 0 is somewhat valid
|
|
||||||
changeOptions({ steps: output }, options, win);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptGlobalShortcuts(win: BrowserWindow, options: ConfigType<'precise-volume'>, item: MenuItem) {
|
|
||||||
const output = await prompt({
|
|
||||||
title: 'Global Volume Keybinds',
|
|
||||||
label: 'Choose Global Volume Keybinds:',
|
|
||||||
type: 'keybind',
|
|
||||||
keybindOptions: [
|
|
||||||
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
|
|
||||||
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
|
|
||||||
],
|
|
||||||
...promptOptions(),
|
|
||||||
}, win);
|
|
||||||
|
|
||||||
if (output) {
|
|
||||||
const newGlobalShortcuts: {
|
|
||||||
volumeUp: string;
|
|
||||||
volumeDown: string;
|
|
||||||
} = { volumeUp: '', volumeDown: '' };
|
|
||||||
for (const { value, accelerator } of output) {
|
|
||||||
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
setConfig(options);
|
||||||
|
|
||||||
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
|
|
||||||
} else {
|
|
||||||
// Reset checkbox if prompt was canceled
|
|
||||||
item.checked = !item.checked;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Helper function for globalShortcuts prompt
|
||||||
|
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
|
||||||
|
|
||||||
|
async function promptVolumeSteps(win: BrowserWindow, options: PreciseVolumePluginConfig) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: 'Volume Steps',
|
||||||
|
label: 'Choose Volume Increase/Decrease Steps',
|
||||||
|
value: options.steps || 1,
|
||||||
|
type: 'counter',
|
||||||
|
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||||
|
width: 380,
|
||||||
|
...promptOptions(),
|
||||||
|
}, win);
|
||||||
|
|
||||||
|
if (output || output === 0) { // 0 is somewhat valid
|
||||||
|
changeOptions({ steps: output }, options, win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptGlobalShortcuts(win: BrowserWindow, options: PreciseVolumePluginConfig, item: MenuItem) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: 'Global Volume Keybinds',
|
||||||
|
label: 'Choose Global Volume Keybinds:',
|
||||||
|
type: 'keybind',
|
||||||
|
keybindOptions: [
|
||||||
|
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
|
||||||
|
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
|
||||||
|
],
|
||||||
|
...promptOptions(),
|
||||||
|
}, win);
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
const newGlobalShortcuts: {
|
||||||
|
volumeUp: string;
|
||||||
|
volumeDown: string;
|
||||||
|
} = { volumeUp: '', volumeDown: '' };
|
||||||
|
for (const { value, accelerator } of output) {
|
||||||
|
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
||||||
|
|
||||||
|
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
|
||||||
|
} else {
|
||||||
|
// Reset checkbox if prompt was canceled
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|||||||
@ -1,270 +1,273 @@
|
|||||||
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(() => {
|
const volumeHud = $<HTMLElement>('#volumeHud');
|
||||||
window.mainConfig.plugins.setOptions('precise-volume', options);
|
if (!volumeHud) {
|
||||||
}, 1000);
|
return;
|
||||||
|
|
||||||
export const moveVolumeHud = debounce((showVideo: boolean) => {
|
|
||||||
const volumeHud = $<HTMLElement>('#volumeHud');
|
|
||||||
if (!volumeHud) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeHud.style.top = showVideo
|
|
||||||
? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px`
|
|
||||||
: '0';
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
|
|
||||||
volumeHud.style.opacity = '0';
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
const hideVolumeSlider = debounce((slider: HTMLElement) => {
|
|
||||||
slider.classList.remove('on-hover');
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
/** Restore saved volume and setup tooltip */
|
|
||||||
function firstRun() {
|
|
||||||
if (typeof options.savedVolume === 'number') {
|
|
||||||
// Set saved volume as tooltip
|
|
||||||
setTooltip(options.savedVolume);
|
|
||||||
|
|
||||||
if (api.getVolume() !== options.savedVolume) {
|
|
||||||
setVolume(options.savedVolume);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setupPlaybar();
|
volumeHud.style.top = showVideo
|
||||||
|
? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px`
|
||||||
|
: '0';
|
||||||
|
}, 250);
|
||||||
|
|
||||||
setupLocalArrowShortcuts();
|
const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
|
||||||
|
volumeHud.style.opacity = '0';
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
|
const hideVolumeSlider = debounce((slider: HTMLElement) => {
|
||||||
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none';
|
slider.classList.remove('on-hover');
|
||||||
injectVolumeHud(noVid);
|
}, 2500);
|
||||||
if (!noVid) {
|
|
||||||
setupVideoPlayerOnwheel();
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change options from renderer to keep sync
|
/** Restore saved volume and setup tooltip */
|
||||||
window.ipcRenderer.on('setOptions', (_event, newOptions = {}) => {
|
function firstRun() {
|
||||||
Object.assign(options, newOptions);
|
if (typeof options.savedVolume === 'number') {
|
||||||
window.mainConfig.plugins.setMenuOptions('precise-volume', options);
|
// Set saved volume as tooltip
|
||||||
});
|
setTooltip(options.savedVolume);
|
||||||
}
|
|
||||||
|
|
||||||
function injectVolumeHud(noVid: boolean) {
|
if (api.getVolume() !== options.savedVolume) {
|
||||||
if (noVid) {
|
setVolume(options.savedVolume);
|
||||||
const position = 'top: 18px; right: 60px;';
|
|
||||||
const mainStyle = 'font-size: xx-large;';
|
|
||||||
|
|
||||||
$('.center-content.ytmusic-nav-bar')?.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const position = 'top: 10px; left: 10px;';
|
|
||||||
const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
|
|
||||||
|
|
||||||
$('#song-video')?.insertAdjacentHTML(
|
|
||||||
'afterend',
|
|
||||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showVolumeHud(volume: number) {
|
|
||||||
const volumeHud = $<HTMLElement>('#volumeHud');
|
|
||||||
if (!volumeHud) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeHud.textContent = `${volume}%`;
|
|
||||||
volumeHud.style.opacity = '1';
|
|
||||||
|
|
||||||
hideVolumeHud(volumeHud);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add onwheel event to video player */
|
|
||||||
function setupVideoPlayerOnwheel() {
|
|
||||||
const panel = $<HTMLElement>('#main-panel');
|
|
||||||
if (!panel) return;
|
|
||||||
|
|
||||||
panel.addEventListener('wheel', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
// Event.deltaY < 0 means wheel-up
|
|
||||||
changeVolume(event.deltaY < 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveVolume(volume: number) {
|
|
||||||
options.savedVolume = volume;
|
|
||||||
writeOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add onwheel event to play bar and also track if play bar is hovered */
|
|
||||||
function setupPlaybar() {
|
|
||||||
const playerbar = $<HTMLElement>('ytmusic-player-bar');
|
|
||||||
if (!playerbar) return;
|
|
||||||
|
|
||||||
playerbar.addEventListener('wheel', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
// Event.deltaY < 0 means wheel-up
|
|
||||||
changeVolume(event.deltaY < 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep track of mouse position for showVolumeSlider()
|
|
||||||
playerbar.addEventListener('mouseenter', () => {
|
|
||||||
playerbar.classList.add('on-hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
playerbar.addEventListener('mouseleave', () => {
|
|
||||||
playerbar.classList.remove('on-hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
setupSliderObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
|
||||||
function setupSliderObserver() {
|
|
||||||
const sliderObserver = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.target instanceof HTMLInputElement) {
|
|
||||||
// This checks that volume-slider was manually set
|
|
||||||
const target = mutation.target;
|
|
||||||
const targetValueNumeric = Number(target.value);
|
|
||||||
if (mutation.oldValue !== target.value
|
|
||||||
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) {
|
|
||||||
// Diff>4 means it was manually set
|
|
||||||
setTooltip(targetValueNumeric);
|
|
||||||
saveVolume(targetValueNumeric);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const slider = $('#volume-slider');
|
setupPlaybar();
|
||||||
if (!slider) return;
|
|
||||||
|
|
||||||
// Observing only changes in 'value' of volume-slider
|
setupLocalArrowShortcuts();
|
||||||
sliderObserver.observe(slider, {
|
|
||||||
attributeFilter: ['value'],
|
|
||||||
attributeOldValue: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVolume(value: number) {
|
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
|
||||||
api.setVolume(value);
|
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none';
|
||||||
// Save the new volume
|
injectVolumeHud(noVid);
|
||||||
saveVolume(value);
|
if (!noVid) {
|
||||||
|
setupVideoPlayerOnwheel();
|
||||||
// Change slider position (important)
|
if (!window.mainConfig.plugins.isEnabled('video-toggle')) {
|
||||||
updateVolumeSlider();
|
// Video-toggle handles hud positioning on its own
|
||||||
|
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
||||||
// Change tooltips to new value
|
$('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode()));
|
||||||
setTooltip(value);
|
|
||||||
// Show volume slider
|
|
||||||
showVolumeSlider();
|
|
||||||
// Show volume HUD
|
|
||||||
showVolumeHud(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If (toIncrease = false) then volume decrease */
|
|
||||||
function changeVolume(toIncrease: boolean) {
|
|
||||||
// Apply volume change if valid
|
|
||||||
const steps = Number(options.steps || 1);
|
|
||||||
setVolume(toIncrease
|
|
||||||
? Math.min(api.getVolume() + steps, 100)
|
|
||||||
: Math.max(api.getVolume() - steps, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVolumeSlider() {
|
|
||||||
const savedVolume = options.savedVolume ?? 0;
|
|
||||||
// Slider value automatically rounds to multiples of 5
|
|
||||||
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
|
|
||||||
const silderElement = $<HTMLInputElement>(slider);
|
|
||||||
if (silderElement) {
|
|
||||||
silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showVolumeSlider() {
|
|
||||||
const slider = $<HTMLElement>('#volume-slider');
|
|
||||||
if (!slider) return;
|
|
||||||
|
|
||||||
// This class display the volume slider if not in minimized mode
|
|
||||||
slider.classList.add('on-hover');
|
|
||||||
|
|
||||||
hideVolumeSlider(slider);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
|
|
||||||
const tooltipTargets = [
|
|
||||||
'#volume-slider',
|
|
||||||
'tp-yt-paper-icon-button.volume',
|
|
||||||
'#expand-volume-slider',
|
|
||||||
'#expand-volume',
|
|
||||||
];
|
|
||||||
|
|
||||||
function setTooltip(volume: number) {
|
|
||||||
for (const target of tooltipTargets) {
|
|
||||||
const tooltipTargetElement = $<HTMLElement>(target);
|
|
||||||
if (tooltipTargetElement) {
|
|
||||||
tooltipTargetElement.title = `${volume}%`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLocalArrowShortcuts() {
|
|
||||||
if (options.arrowsShortcut) {
|
|
||||||
window.addEventListener('keydown', (event) => {
|
|
||||||
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.code) {
|
function injectVolumeHud(noVid: boolean) {
|
||||||
case 'ArrowUp': {
|
if (noVid) {
|
||||||
event.preventDefault();
|
const position = 'top: 18px; right: 60px;';
|
||||||
changeVolume(true);
|
const mainStyle = 'font-size: xx-large;';
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ArrowDown': {
|
$('.center-content.ytmusic-nav-bar')?.insertAdjacentHTML(
|
||||||
event.preventDefault();
|
'beforeend',
|
||||||
changeVolume(false);
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
||||||
break;
|
);
|
||||||
|
} else {
|
||||||
|
const position = 'top: 10px; left: 10px;';
|
||||||
|
const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
|
||||||
|
|
||||||
|
$('#song-video')?.insertAdjacentHTML(
|
||||||
|
'afterend',
|
||||||
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showVolumeHud(volume: number) {
|
||||||
|
const volumeHud = $<HTMLElement>('#volumeHud');
|
||||||
|
if (!volumeHud) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeHud.textContent = `${volume}%`;
|
||||||
|
volumeHud.style.opacity = '1';
|
||||||
|
|
||||||
|
hideVolumeHud(volumeHud);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add onwheel event to video player */
|
||||||
|
function setupVideoPlayerOnwheel() {
|
||||||
|
const panel = $<HTMLElement>('#main-panel');
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
panel.addEventListener('wheel', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Event.deltaY < 0 means wheel-up
|
||||||
|
changeVolume(event.deltaY < 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveVolume(volume: number) {
|
||||||
|
options.savedVolume = volume;
|
||||||
|
writeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add onwheel event to play bar and also track if play bar is hovered */
|
||||||
|
function setupPlaybar() {
|
||||||
|
const playerbar = $<HTMLElement>('ytmusic-player-bar');
|
||||||
|
if (!playerbar) return;
|
||||||
|
|
||||||
|
playerbar.addEventListener('wheel', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Event.deltaY < 0 means wheel-up
|
||||||
|
changeVolume(event.deltaY < 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep track of mouse position for showVolumeSlider()
|
||||||
|
playerbar.addEventListener('mouseenter', () => {
|
||||||
|
playerbar.classList.add('on-hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
playerbar.addEventListener('mouseleave', () => {
|
||||||
|
playerbar.classList.remove('on-hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
setupSliderObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
||||||
|
function setupSliderObserver() {
|
||||||
|
const sliderObserver = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.target instanceof HTMLInputElement) {
|
||||||
|
// This checks that volume-slider was manually set
|
||||||
|
const target = mutation.target;
|
||||||
|
const targetValueNumeric = Number(target.value);
|
||||||
|
if (mutation.oldValue !== target.value
|
||||||
|
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) {
|
||||||
|
// Diff>4 means it was manually set
|
||||||
|
setTooltip(targetValueNumeric);
|
||||||
|
saveVolume(targetValueNumeric);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slider = $('#volume-slider');
|
||||||
|
if (!slider) return;
|
||||||
|
|
||||||
|
// Observing only changes in 'value' of volume-slider
|
||||||
|
sliderObserver.observe(slider, {
|
||||||
|
attributeFilter: ['value'],
|
||||||
|
attributeOldValue: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
function setVolume(value: number) {
|
||||||
|
api.setVolume(value);
|
||||||
|
// Save the new volume
|
||||||
|
saveVolume(value);
|
||||||
|
|
||||||
|
// Change slider position (important)
|
||||||
|
updateVolumeSlider();
|
||||||
|
|
||||||
|
// Change tooltips to new value
|
||||||
|
setTooltip(value);
|
||||||
|
// Show volume slider
|
||||||
|
showVolumeSlider();
|
||||||
|
// Show volume HUD
|
||||||
|
showVolumeHud(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If (toIncrease = false) then volume decrease */
|
||||||
|
function changeVolume(toIncrease: boolean) {
|
||||||
|
// Apply volume change if valid
|
||||||
|
const steps = Number(options.steps || 1);
|
||||||
|
setVolume(toIncrease
|
||||||
|
? Math.min(api.getVolume() + steps, 100)
|
||||||
|
: Math.max(api.getVolume() - steps, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolumeSlider() {
|
||||||
|
const savedVolume = options.savedVolume ?? 0;
|
||||||
|
// Slider value automatically rounds to multiples of 5
|
||||||
|
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
|
||||||
|
const silderElement = $<HTMLInputElement>(slider);
|
||||||
|
if (silderElement) {
|
||||||
|
silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showVolumeSlider() {
|
||||||
|
const slider = $<HTMLElement>('#volume-slider');
|
||||||
|
if (!slider) return;
|
||||||
|
|
||||||
|
// This class display the volume slider if not in minimized mode
|
||||||
|
slider.classList.add('on-hover');
|
||||||
|
|
||||||
|
hideVolumeSlider(slider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
|
||||||
|
const tooltipTargets = [
|
||||||
|
'#volume-slider',
|
||||||
|
'tp-yt-paper-icon-button.volume',
|
||||||
|
'#expand-volume-slider',
|
||||||
|
'#expand-volume',
|
||||||
|
];
|
||||||
|
|
||||||
|
function setTooltip(volume: number) {
|
||||||
|
for (const target of tooltipTargets) {
|
||||||
|
const tooltipTargetElement = $<HTMLElement>(target);
|
||||||
|
if (tooltipTargetElement) {
|
||||||
|
tooltipTargetElement.title = `${volume}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLocalArrowShortcuts() {
|
||||||
|
if (options.arrowsShortcut) {
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowUp': {
|
||||||
|
event.preventDefault();
|
||||||
|
changeVolume(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ArrowDown': {
|
||||||
|
event.preventDefault();
|
||||||
|
changeVolume(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>>;
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
21
src/virtual-module.d.ts
vendored
21
src/virtual-module.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
@ -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 += '};';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user