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

@ -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,17 +1,24 @@
export default () =>
document.addEventListener('audioCanPlay', (e) => {
const { audioContext } = e.detail;
import builder from '.';
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.ratio.value = 12;
compressor.knee.value = 40;
compressor.attack.value = 0;
compressor.release.value = 0.25;
export default builder.createRenderer(() => {
return {
onLoad() {
document.addEventListener('audioCanPlay', (e) => {
const { audioContext } = e.detail;
e.detail.audioSource.connect(compressor);
compressor.connect(audioContext.destination);
}, {
once: true, // Only create the audio compressor once, not on each video
passive: true,
});
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.ratio.value = 12;
compressor.knee.value = 40;
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,
});
}
};
});

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 () => {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
await import('simple-youtube-age-restriction-bypass');
};
import builder from '.';
export default builder.createRenderer(() => ({
async onLoad() {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
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,75 +2,75 @@ import { register } from 'electron-localshortcut';
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.webContents.send('close-all-in-app-menu-panel');
});
win.on('close', () => {
win.webContents.send('close-all-in-app-menu-panel');
});
win.once('ready-to-show', () => {
register(win, '`', () => {
win.webContents.send('toggle-in-app-menu');
});
});
win.once('ready-to-show', () => {
register(win, '`', () => {
win.webContents.send('toggle-in-app-menu');
});
});
handle(
'get-menu',
() => JSON.parse(JSON.stringify(
Menu.getApplicationMenu(),
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
),
);
ipcMain.handle(
'get-menu',
() => JSON.parse(JSON.stringify(
Menu.getApplicationMenu(),
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
),
);
const getMenuItemById = (commandId: number): MenuItem | null => {
const menu = Menu.getApplicationMenu();
const getMenuItemById = (commandId: number): MenuItem | null => {
const menu = Menu.getApplicationMenu();
let target: MenuItem | null = null;
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;
const stack = [...menu?.items ?? []];
while (stack.length > 0) {
const now = stack.shift();
now?.submenu?.items.forEach((item) => stack.push(item));
if (now?.commandId === commandId) {
target = now;
break;
}
}
if (now?.commandId === commandId) {
target = now;
break;
}
}
return target;
};
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();
});
};
});

View File

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

View File

@ -6,186 +6,191 @@ import minimizeRaw from './assets/minimize.svg?inline';
import maximizeRaw from './assets/maximize.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) {
return document.querySelector<E>(selector);
}
import type { Menu } from 'electron';
const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
export default async () => {
const hideDOMWindowControls = window.mainConfig.get('plugins.in-app-menu.hideDOMWindowControls');
let hideMenu = window.mainConfig.get('options.hideMenu');
const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
let maximizeButton: HTMLButtonElement;
let panelClosers: (() => void)[] = [];
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
export default builder.createRenderer(({ getConfig, invoke, on }) => {
return {
async onLoad() {
const config = await getConfig();
const hideDOMWindowControls = config.hideDOMWindowControls;
const logo = document.createElement('img');
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) {
logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw;
close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw;
minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw;
maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw;
unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw;
} else {
logo.src = logoRaw;
close.src = closeRaw;
minimize.src = minimizeRaw;
maximize.src = maximizeRaw;
unmaximize.src = unmaximizeRaw;
}
logo.classList.add('title-bar-icon');
const logoClick = () => {
hideMenu = !hideMenu;
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;
window.ipcRenderer.on('toggle-in-app-menu', logoClick);
if (!isMacOS) titleBar.appendChild(logo);
document.body.appendChild(titleBar);
titleBar.appendChild(logo);
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');
let hideMenu = window.mainConfig.get('options.hideMenu');
const titleBar = document.createElement('title-bar');
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 close = document.createElement('img');
const minimize = document.createElement('img');
const maximize = document.createElement('img');
const unmaximize = document.createElement('img');
if (window.ELECTRON_RENDERER_URL) {
logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw;
close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw;
minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw;
maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw;
unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw;
} else {
// change icon to unmaximize
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(unmaximize);
// call maximize
await window.ipcRenderer.invoke('window-maximize');
logo.src = logoRaw;
close.src = closeRaw;
minimize.src = minimizeRaw;
maximize.src = maximizeRaw;
unmaximize.src = unmaximizeRaw;
}
};
logo.classList.add('title-bar-icon');
const logoClick = () => {
hideMenu = !hideMenu;
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;
on('toggle-in-app-menu', logoClick);
if (!isMacOS) titleBar.appendChild(logo);
document.body.appendChild(titleBar);
titleBar.appendChild(logo);
const addWindowControls = async () => {
// Create window control buttons
const minimizeButton = document.createElement('button');
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();
const closeButton = document.createElement('button');
closeButton.classList.add('window-control');
closeButton.appendChild(close);
closeButton.onclick = () => window.ipcRenderer.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);
document.title = 'Youtube Music';
on('close-all-in-app-menu-panel', () => {
panelClosers.forEach((closer) => closer());
});
});
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 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';
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);
}
});
if (window.mainConfig.plugins.isEnabled('picture-in-picture')) {
on('pip-toggle', () => {
updateMenu();
});
}
});
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 });
};
});

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';
/*
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) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
}
};
if (config.globalShortcuts?.volumeUp) {
globalShortcut.register(config.globalShortcuts.volumeUp, () => send('changeVolume', true));
}
if (config.globalShortcuts?.volumeDown) {
globalShortcut.register(config.globalShortcuts.volumeDown, () => send('changeVolume', false));
}
},
};
});

View File

@ -2,93 +2,89 @@ import prompt, { KeybindOptions } from 'custom-electron-prompt';
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 { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic';
function changeOptions(changedOptions: Partial<ConfigType<'precise-volume'>>, options: ConfigType<'precise-volume'>, win: BrowserWindow) {
for (const option in changedOptions) {
// HACK: Weird TypeScript error
(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;
export default builder.createMenu(async ({ setConfig, getConfig, window }) => {
const config = await getConfig();
function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig, win: BrowserWindow) {
for (const option in changedOptions) {
// HACK: Weird TypeScript error
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option];
}
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;
setConfig(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: 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),
},
];
});

View File

@ -1,270 +1,273 @@
import { overrideListener } from './override';
import builder, { type PreciseVolumePluginConfig } from './';
import { debounce } from '../../providers/decorators';
import type { YoutubePlayer } from '../../types/youtube-player';
import type { ConfigType } from '../../config/dynamic';
function $<E extends Element = Element>(selector: string) {
return document.querySelector<E>(selector);
}
let api: YoutubePlayer;
let options: ConfigType<'precise-volume'>;
export default (_options: ConfigType<'precise-volume'>) => {
overrideListener();
export default builder.createRenderer(async ({ on, getConfig, setConfig }) => {
let options: PreciseVolumePluginConfig = await getConfig();
options = _options;
document.addEventListener('apiLoaded', (e) => {
api = e.detail;
window.ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease));
window.ipcRenderer.on('setVolume', (_, value: number) => setVolume(value));
firstRun();
}, { once: true, passive: true });
};
// Without this function it would rewrite config 20 time when volume change by 20
const writeOptions = debounce(() => {
window.mainConfig.plugins.setOptions('precise-volume', options);
}, 1000);
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);
// Without this function it would rewrite config 20 time when volume change by 20
const writeOptions = debounce(() => {
setConfig(options);
}, 1000);
const moveVolumeHud = debounce((showVideo: boolean) => {
const volumeHud = $<HTMLElement>('#volumeHud');
if (!volumeHud) {
return;
}
}
setupPlaybar();
setupLocalArrowShortcuts();
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none';
injectVolumeHud(noVid);
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
window.ipcRenderer.on('setOptions', (_event, newOptions = {}) => {
Object.assign(options, newOptions);
window.mainConfig.plugins.setMenuOptions('precise-volume', options);
});
}
function injectVolumeHud(noVid: boolean) {
if (noVid) {
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);
}
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);
}
}
});
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;
setupPlaybar();
setupLocalArrowShortcuts();
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none';
injectVolumeHud(noVid);
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()));
}
switch (event.code) {
case 'ArrowUp': {
event.preventDefault();
changeVolume(true);
break;
}
case 'ArrowDown': {
event.preventDefault();
changeVolume(false);
break;
}
}
function injectVolumeHud(noVid: boolean) {
if (noVid) {
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');
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;
}
};
});

View File

@ -43,7 +43,6 @@ export default builder.createRenderer(({ invoke }) => {
return {
onLoad() {
console.log('qc');
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>;
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 MainPluginFactory<Config extends PluginBaseConfig> = (context: MainPluginContext<Config>) => Promisable<MainPlugin<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> = {
createRenderer: IF<RendererPluginFactory<Config>>;

View File

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

View File

@ -2,14 +2,15 @@ import buttonTemplate from './templates/button_template.html?raw';
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 { ThumbnailElement } from '../../types/get-player-response';
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 {
return document.querySelector<E>(selector);