mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
feat(plugin): migrating plugins to new plugin system (WIP)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -10,6 +10,9 @@ import { restart } from '../providers/app-controls';
|
|||||||
const set = (key: string, value: unknown) => {
|
const set = (key: string, value: unknown) => {
|
||||||
store.set(key, value);
|
store.set(key, value);
|
||||||
};
|
};
|
||||||
|
const setPartial = (value: object) => {
|
||||||
|
store.set(value);
|
||||||
|
};
|
||||||
|
|
||||||
function setMenuOption(key: string, value: unknown) {
|
function setMenuOption(key: string, value: unknown) {
|
||||||
set(key, value);
|
set(key, value);
|
||||||
@ -43,6 +46,7 @@ export default {
|
|||||||
defaultConfig,
|
defaultConfig,
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
|
setPartial,
|
||||||
setMenuOption,
|
setMenuOption,
|
||||||
edit: () => store.openInEditor(),
|
edit: () => store.openInEditor(),
|
||||||
watch(cb: Parameters<Store['onDidChange']>[1]) {
|
watch(cb: Parameters<Store['onDidChange']>[1]) {
|
||||||
|
|||||||
84
src/index.ts
84
src/index.ts
@ -22,10 +22,14 @@ import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/
|
|||||||
|
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { mainPlugins } from 'virtual:MainPlugins';
|
import { mainPlugins } from 'virtual:MainPlugins';
|
||||||
|
import ambientModeMainPluginBuilder from './plugins/ambient-mode/index';
|
||||||
|
import qualityChangerMainPluginBuilder from './plugins/quality-changer/index';
|
||||||
|
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';
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -96,6 +100,34 @@ 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) => {
|
||||||
|
ipcMain.handle('get-config', (_, name: string) => config.get(`plugins.${name}` as never));
|
||||||
|
ipcMain.handle('set-config', (_, name: string, obj: object) => config.setPartial({
|
||||||
|
plugins: {
|
||||||
|
[name]: obj,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
config.watch((newValue) => {
|
||||||
|
const value = newValue as Record<string, unknown>;
|
||||||
|
const target = pluginBuilderList.find(([name]) => name in value);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
win.webContents.send('config-changed', target[0], value[target[0]]);
|
||||||
|
console.log('config-changed', target[0], value[target[0]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadedPluginList: [string, MainPlugin<PluginBaseConfig>][] = [];
|
||||||
async function loadPlugins(win: BrowserWindow) {
|
async function loadPlugins(win: BrowserWindow) {
|
||||||
injectCSS(win.webContents, youtubeMusicCSS);
|
injectCSS(win.webContents, youtubeMusicCSS);
|
||||||
// Load user CSS
|
// Load user CSS
|
||||||
@ -121,7 +153,58 @@ async function loadPlugins(win: BrowserWindow) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const createContext = <
|
||||||
|
Key extends keyof PluginBuilderList,
|
||||||
|
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
||||||
|
>(name: Key): MainPluginContext<Config> => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
getConfig: () => config.get(`plugins.${name}`) as unknown as Config,
|
||||||
|
setConfig: (newConfig) => {
|
||||||
|
config.setPartial({
|
||||||
|
plugins: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
[name]: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
...config.get(`plugins.${name}`),
|
||||||
|
...newConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
send: (event: string, ...args: unknown[]) => {
|
||||||
|
win.webContents.send(event, ...args);
|
||||||
|
},
|
||||||
|
handle: (event: string, listener) => {
|
||||||
|
ipcMain.handle(event, async (_, ...args) => listener(...args as never));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
for (const [plugin, options] of config.plugins.getEnabled()) {
|
for (const [plugin, options] of config.plugins.getEnabled()) {
|
||||||
|
const builderTarget = pluginBuilderList.find(([name]) => name === plugin);
|
||||||
|
const mainPluginTarget = mainPluginList.find(([name]) => name === plugin);
|
||||||
|
|
||||||
|
if (mainPluginTarget) {
|
||||||
|
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) => {
|
||||||
|
injectCSS(win.webContents, style);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Object.hasOwn(mainPlugins, plugin)) {
|
if (Object.hasOwn(mainPlugins, plugin)) {
|
||||||
console.log('Loaded plugin - ' + plugin);
|
console.log('Loaded plugin - ' + plugin);
|
||||||
@ -174,6 +257,7 @@ async function createMainWindow() {
|
|||||||
: 'default'),
|
: 'default'),
|
||||||
autoHideMenuBar: config.get('options.hideMenu'),
|
autoHideMenuBar: config.get('options.hideMenu'),
|
||||||
});
|
});
|
||||||
|
initHook(win);
|
||||||
await loadPlugins(win);
|
await loadPlugins(win);
|
||||||
|
|
||||||
if (windowPosition) {
|
if (windowPosition) {
|
||||||
|
|||||||
61
src/menu.ts
61
src/menu.ts
@ -1,5 +1,5 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
import { app, BrowserWindow, clipboard, dialog, ipcMain, 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';
|
||||||
@ -10,7 +10,10 @@ import promptOptions from './providers/prompt-options';
|
|||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { menuPlugins as menuList } from 'virtual:MenuPlugins';
|
import { menuPlugins as menuList } from 'virtual:MenuPlugins';
|
||||||
|
|
||||||
|
import ambientModeMenuPlugin from './plugins/ambient-mode/menu';
|
||||||
|
|
||||||
import { getAvailablePluginNames } from './plugins/utils/main';
|
import { getAvailablePluginNames } from './plugins/utils/main';
|
||||||
|
import { PluginBaseConfig, PluginContext } from './plugins/utils/builder';
|
||||||
|
|
||||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||||
|
|
||||||
@ -43,14 +46,58 @@ export const refreshMenu = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mainMenuTemplate = (win: BrowserWindow): 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> => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
getConfig: () => config.get(`plugins.${name}`) as unknown as Config,
|
||||||
|
setConfig: (newConfig) => {
|
||||||
|
config.setPartial({
|
||||||
|
plugins: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
[name]: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
...config.get(`plugins.${name}`),
|
||||||
|
...newConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginMenus = await Promise.all(
|
||||||
|
[['ambient-mode', ambientModeMenuPlugin] as const].map(async ([id, plugin]) => {
|
||||||
|
let pluginLabel = id;
|
||||||
|
if (betaPlugins.includes(pluginLabel)) {
|
||||||
|
pluginLabel += ' [beta]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.plugins.isEnabled(id)) {
|
||||||
|
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = await plugin(createContext(id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: pluginLabel,
|
||||||
|
submenu: [
|
||||||
|
pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu),
|
||||||
|
{ type: 'separator' },
|
||||||
|
...template,
|
||||||
|
],
|
||||||
|
} satisfies Electron.MenuItemConstructorOptions;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Plugins',
|
label: 'Plugins',
|
||||||
submenu:
|
submenu: [
|
||||||
getAvailablePluginNames().map((pluginName) => {
|
...getAvailablePluginNames().map((pluginName) => {
|
||||||
let pluginLabel = pluginName;
|
let pluginLabel = pluginName;
|
||||||
if (betaPlugins.includes(pluginLabel)) {
|
if (betaPlugins.includes(pluginLabel)) {
|
||||||
pluginLabel += ' [beta]';
|
pluginLabel += ' [beta]';
|
||||||
@ -75,6 +122,8 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
|||||||
|
|
||||||
return pluginEnabledMenu(pluginName, pluginLabel);
|
return pluginEnabledMenu(pluginName, pluginLabel);
|
||||||
}),
|
}),
|
||||||
|
...pluginMenus,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Options',
|
label: 'Options',
|
||||||
@ -402,8 +451,8 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
|
||||||
const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
|
const menuTemplate: MenuTemplate = [...await mainMenuTemplate(win)];
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const { name } = app;
|
const { name } = app;
|
||||||
menuTemplate.unshift({
|
menuTemplate.unshift({
|
||||||
|
|||||||
19
src/plugins/album-color-theme/index.ts
Normal file
19
src/plugins/album-color-theme/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import style from './style.css?inline';
|
||||||
|
|
||||||
|
import { createPluginBuilder } from '../utils/builder';
|
||||||
|
|
||||||
|
const builder = createPluginBuilder('album-color-theme', {
|
||||||
|
name: 'Album Color Theme',
|
||||||
|
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);
|
|
||||||
};
|
|
||||||
@ -1,127 +1,130 @@
|
|||||||
import { FastAverageColor } from 'fast-average-color';
|
import { FastAverageColor } from 'fast-average-color';
|
||||||
|
|
||||||
import type { ConfigType } from '../../config/dynamic';
|
import builder from './';
|
||||||
|
|
||||||
function hexToHSL(H: string) {
|
export default builder.createRenderer(() => {
|
||||||
// Convert hex to RGB first
|
function hexToHSL(H: string) {
|
||||||
let r = 0;
|
// Convert hex to RGB first
|
||||||
let g = 0;
|
let r = 0;
|
||||||
let b = 0;
|
let g = 0;
|
||||||
if (H.length == 4) {
|
let b = 0;
|
||||||
r = Number('0x' + H[1] + H[1]);
|
if (H.length == 4) {
|
||||||
g = Number('0x' + H[2] + H[2]);
|
r = Number('0x' + H[1] + H[1]);
|
||||||
b = Number('0x' + H[3] + H[3]);
|
g = Number('0x' + H[2] + H[2]);
|
||||||
} else if (H.length == 7) {
|
b = Number('0x' + H[3] + H[3]);
|
||||||
r = Number('0x' + H[1] + H[2]);
|
} else if (H.length == 7) {
|
||||||
g = Number('0x' + H[3] + H[4]);
|
r = Number('0x' + H[1] + H[2]);
|
||||||
b = Number('0x' + H[5] + H[6]);
|
g = Number('0x' + H[3] + H[4]);
|
||||||
|
b = Number('0x' + H[5] + H[6]);
|
||||||
|
}
|
||||||
|
// Then to HSL
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
const cmin = Math.min(r, g, b);
|
||||||
|
const cmax = Math.max(r, g, b);
|
||||||
|
const delta = cmax - cmin;
|
||||||
|
let h: number;
|
||||||
|
let s: number;
|
||||||
|
let l: number;
|
||||||
|
|
||||||
|
if (delta == 0) {
|
||||||
|
h = 0;
|
||||||
|
} else if (cmax == r) {
|
||||||
|
h = ((g - b) / delta) % 6;
|
||||||
|
} else if (cmax == g) {
|
||||||
|
h = ((b - r) / delta) + 2;
|
||||||
|
} else {
|
||||||
|
h = ((r - g) / delta) + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h = Math.round(h * 60);
|
||||||
|
|
||||||
|
if (h < 0) {
|
||||||
|
h += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
l = (cmax + cmin) / 2;
|
||||||
|
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
|
||||||
|
s = +(s * 100).toFixed(1);
|
||||||
|
l = +(l * 100).toFixed(1);
|
||||||
|
|
||||||
|
//return "hsl(" + h + "," + s + "%," + l + "%)";
|
||||||
|
return [h,s,l];
|
||||||
}
|
}
|
||||||
// Then to HSL
|
|
||||||
r /= 255;
|
let hue = 0;
|
||||||
g /= 255;
|
let saturation = 0;
|
||||||
b /= 255;
|
let lightness = 0;
|
||||||
const cmin = Math.min(r, g, b);
|
|
||||||
const cmax = Math.max(r, g, b);
|
function changeElementColor(element: HTMLElement | null, hue: number, saturation: number, lightness: number){
|
||||||
const delta = cmax - cmin;
|
if (element) {
|
||||||
let h: number;
|
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
let s: number;
|
}
|
||||||
let l: number;
|
|
||||||
|
|
||||||
if (delta == 0) {
|
|
||||||
h = 0;
|
|
||||||
} else if (cmax == r) {
|
|
||||||
h = ((g - b) / delta) % 6;
|
|
||||||
} else if (cmax == g) {
|
|
||||||
h = ((b - r) / delta) + 2;
|
|
||||||
} else {
|
|
||||||
h = ((r - g) / delta) + 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onLoad() {
|
||||||
|
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||||
|
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||||
|
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||||
|
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||||
|
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||||
|
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||||
|
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||||
|
|
||||||
h = Math.round(h * 60);
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
if (h < 0) {
|
if (mutation.type === 'attributes') {
|
||||||
h += 360;
|
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||||
}
|
if (isPageOpen) {
|
||||||
|
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
||||||
l = (cmax + cmin) / 2;
|
} else {
|
||||||
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
|
if (sidebarSmall) {
|
||||||
s = +(s * 100).toFixed(1);
|
sidebarSmall.style.backgroundColor = 'black';
|
||||||
l = +(l * 100).toFixed(1);
|
}
|
||||||
|
}
|
||||||
//return "hsl(" + h + "," + s + "%," + l + "%)";
|
|
||||||
return [h,s,l];
|
|
||||||
}
|
|
||||||
|
|
||||||
let hue = 0;
|
|
||||||
let saturation = 0;
|
|
||||||
let lightness = 0;
|
|
||||||
|
|
||||||
function changeElementColor(element: HTMLElement | null, hue: number, saturation: number, lightness: number){
|
|
||||||
if (element) {
|
|
||||||
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (_: ConfigType<'album-color-theme'>) => {
|
|
||||||
// updated elements
|
|
||||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
|
||||||
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
|
||||||
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
|
||||||
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
|
||||||
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
|
||||||
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
|
||||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutationsList) => {
|
|
||||||
for (const mutation of mutationsList) {
|
|
||||||
if (mutation.type === 'attributes') {
|
|
||||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
|
||||||
if (isPageOpen) {
|
|
||||||
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
|
||||||
} else {
|
|
||||||
if (sidebarSmall) {
|
|
||||||
sidebarSmall.style.backgroundColor = 'black';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playerPage) {
|
||||||
|
observer.observe(playerPage, { attributes: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||||
|
const fastAverageColor = new FastAverageColor();
|
||||||
|
|
||||||
|
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
||||||
|
if (name === 'dataloaded') {
|
||||||
|
const playerResponse = apiEvent.detail.getPlayerResponse();
|
||||||
|
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||||
|
if (thumbnail) {
|
||||||
|
fastAverageColor.getColorAsync(thumbnail.url)
|
||||||
|
.then((albumColor) => {
|
||||||
|
if (albumColor) {
|
||||||
|
[hue, saturation, lightness] = hexToHSL(albumColor.hex);
|
||||||
|
changeElementColor(playerPage, hue, saturation, lightness - 30);
|
||||||
|
changeElementColor(navBarBackground, hue, saturation, lightness - 15);
|
||||||
|
changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15);
|
||||||
|
changeElementColor(playerBarBackground, hue, saturation, lightness - 15);
|
||||||
|
changeElementColor(sidebarBig, hue, saturation, lightness - 15);
|
||||||
|
if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
|
||||||
|
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
||||||
|
}
|
||||||
|
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
|
||||||
|
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
|
||||||
|
} else {
|
||||||
|
if (playerPage) {
|
||||||
|
playerPage.style.backgroundColor = '#000000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
});
|
||||||
if (playerPage) {
|
|
||||||
observer.observe(playerPage, { attributes: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
|
||||||
const fastAverageColor = new FastAverageColor();
|
|
||||||
|
|
||||||
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
|
||||||
if (name === 'dataloaded') {
|
|
||||||
const playerResponse = apiEvent.detail.getPlayerResponse();
|
|
||||||
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
|
||||||
if (thumbnail) {
|
|
||||||
fastAverageColor.getColorAsync(thumbnail.url)
|
|
||||||
.then((albumColor) => {
|
|
||||||
if (albumColor) {
|
|
||||||
[hue, saturation, lightness] = hexToHSL(albumColor.hex);
|
|
||||||
changeElementColor(playerPage, hue, saturation, lightness - 30);
|
|
||||||
changeElementColor(navBarBackground, hue, saturation, lightness - 15);
|
|
||||||
changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15);
|
|
||||||
changeElementColor(playerBarBackground, hue, saturation, lightness - 15);
|
|
||||||
changeElementColor(sidebarBig, hue, saturation, lightness - 15);
|
|
||||||
if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
|
|
||||||
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
|
||||||
}
|
|
||||||
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
|
|
||||||
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
|
|
||||||
} else {
|
|
||||||
if (playerPage) {
|
|
||||||
playerPage.style.backgroundColor = '#000000';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import style from './style.css';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
import { createPluginBuilder } from '../utils/builder';
|
import { createPluginBuilder } from '../utils/builder';
|
||||||
|
|
||||||
|
|||||||
@ -7,79 +7,83 @@ const bufferList = [1, 5, 10, 20, 30];
|
|||||||
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
|
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
|
||||||
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
|
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
|
||||||
|
|
||||||
export default builder.createMenu(({ getConfig, setConfig }) => ([
|
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||||
{
|
const config = await getConfig();
|
||||||
label: 'Smoothness transition',
|
|
||||||
submenu: interpolationTimeList.map((interpolationTime) => ({
|
return [
|
||||||
label: `During ${interpolationTime / 1000}s`,
|
{
|
||||||
type: 'radio',
|
label: 'Smoothness transition',
|
||||||
checked: getConfig().interpolationTime === interpolationTime,
|
submenu: interpolationTimeList.map((interpolationTime) => ({
|
||||||
click() {
|
label: `During ${interpolationTime / 1000}s`,
|
||||||
setConfig({ interpolationTime });
|
type: 'radio',
|
||||||
},
|
checked: config.interpolationTime === interpolationTime,
|
||||||
})),
|
click() {
|
||||||
},
|
setConfig({ interpolationTime });
|
||||||
{
|
},
|
||||||
label: 'Quality',
|
})),
|
||||||
submenu: qualityList.map((quality) => ({
|
|
||||||
label: `${quality} pixels`,
|
|
||||||
type: 'radio',
|
|
||||||
checked: getConfig().quality === quality,
|
|
||||||
click() {
|
|
||||||
setConfig({ quality });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Size',
|
|
||||||
submenu: sizeList.map((size) => ({
|
|
||||||
label: `${size}%`,
|
|
||||||
type: 'radio',
|
|
||||||
checked: getConfig().size === size,
|
|
||||||
click() {
|
|
||||||
setConfig({ size });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Buffer',
|
|
||||||
submenu: bufferList.map((buffer) => ({
|
|
||||||
label: `${buffer}`,
|
|
||||||
type: 'radio',
|
|
||||||
checked: getConfig().buffer === buffer,
|
|
||||||
click() {
|
|
||||||
setConfig({ buffer });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Opacity',
|
|
||||||
submenu: opacityList.map((opacity) => ({
|
|
||||||
label: `${opacity * 100}%`,
|
|
||||||
type: 'radio',
|
|
||||||
checked: getConfig().opacity === opacity,
|
|
||||||
click() {
|
|
||||||
setConfig({ opacity });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Blur amount',
|
|
||||||
submenu: blurAmountList.map((blur) => ({
|
|
||||||
label: `${blur} pixels`,
|
|
||||||
type: 'radio',
|
|
||||||
checked: getConfig().blur === blur,
|
|
||||||
click() {
|
|
||||||
setConfig({ blur });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Using fullscreen',
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: getConfig().fullscreen,
|
|
||||||
click(item) {
|
|
||||||
setConfig({ fullscreen: item.checked });
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
]));
|
label: 'Quality',
|
||||||
|
submenu: qualityList.map((quality) => ({
|
||||||
|
label: `${quality} pixels`,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.quality === quality,
|
||||||
|
click() {
|
||||||
|
setConfig({ quality });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Size',
|
||||||
|
submenu: sizeList.map((size) => ({
|
||||||
|
label: `${size}%`,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.size === size,
|
||||||
|
click() {
|
||||||
|
setConfig({ size });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Buffer',
|
||||||
|
submenu: bufferList.map((buffer) => ({
|
||||||
|
label: `${buffer}`,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.buffer === buffer,
|
||||||
|
click() {
|
||||||
|
setConfig({ buffer });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opacity',
|
||||||
|
submenu: opacityList.map((opacity) => ({
|
||||||
|
label: `${opacity * 100}%`,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.opacity === opacity,
|
||||||
|
click() {
|
||||||
|
setConfig({ opacity });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Blur amount',
|
||||||
|
submenu: blurAmountList.map((blur) => ({
|
||||||
|
label: `${blur} pixels`,
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.blur === blur,
|
||||||
|
click() {
|
||||||
|
setConfig({ blur });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Using fullscreen',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: config.fullscreen,
|
||||||
|
click(item) {
|
||||||
|
setConfig({ fullscreen: item.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import builder from './index';
|
import builder from './index';
|
||||||
|
|
||||||
export default builder.createRenderer(({ getConfig }) => {
|
export default builder.createRenderer(async ({ getConfig }) => {
|
||||||
const initConfigData = getConfig();
|
const initConfigData = await getConfig();
|
||||||
|
|
||||||
let interpolationTime = initConfigData.interpolationTime;
|
let interpolationTime = initConfigData.interpolationTime;
|
||||||
let buffer = initConfigData.buffer;
|
let buffer = initConfigData.buffer;
|
||||||
@ -26,6 +26,7 @@ export default builder.createRenderer(({ getConfig }) => {
|
|||||||
if (!video) return null;
|
if (!video) return null;
|
||||||
if (!wrapper) return null;
|
if (!wrapper) return null;
|
||||||
|
|
||||||
|
console.log('injectBlurVideo', songVideo, video, wrapper);
|
||||||
const blurCanvas = document.createElement('canvas');
|
const blurCanvas = document.createElement('canvas');
|
||||||
blurCanvas.classList.add('html5-blur-canvas');
|
blurCanvas.classList.add('html5-blur-canvas');
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ export default builder.createRenderer(({ getConfig }) => {
|
|||||||
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
|
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
|
||||||
|
|
||||||
lastEffectWorkId = requestAnimationFrame(() => {
|
lastEffectWorkId = requestAnimationFrame(() => {
|
||||||
|
// console.log('context', context);
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const width = qualityRatio;
|
const width = qualityRatio;
|
||||||
|
|||||||
16
src/plugins/quality-changer/index.ts
Normal file
16
src/plugins/quality-changer/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createPluginBuilder } from '../utils/builder';
|
||||||
|
|
||||||
|
const builder = createPluginBuilder('quality-changer', {
|
||||||
|
name: 'Quality Changer',
|
||||||
|
config: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default builder;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface PluginBuilderList {
|
||||||
|
[builder.id]: typeof builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,17 @@
|
|||||||
import { ipcMain, dialog, BrowserWindow } from 'electron';
|
import { dialog, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
export default (win: BrowserWindow) => {
|
import builder from './index';
|
||||||
ipcMain.handle('qualityChanger', async (_, qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(win, {
|
|
||||||
type: 'question',
|
export default builder.createMain(({ handle }) => ({
|
||||||
buttons: qualityLabels,
|
onLoad(win: BrowserWindow) {
|
||||||
defaultId: currentIndex,
|
handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(win, {
|
||||||
title: 'Choose Video Quality',
|
type: 'question',
|
||||||
message: 'Choose Video Quality:',
|
buttons: qualityLabels,
|
||||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
defaultId: currentIndex,
|
||||||
cancelId: -1,
|
title: 'Choose Video Quality',
|
||||||
}));
|
message: 'Choose Video Quality:',
|
||||||
};
|
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||||
|
cancelId: -1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|||||||
@ -1,38 +1,50 @@
|
|||||||
import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
|
||||||
|
|
||||||
|
import builder from './';
|
||||||
|
|
||||||
import { ElementFromHtml } from '../utils/renderer';
|
import { ElementFromHtml } from '../utils/renderer';
|
||||||
import { YoutubePlayer } from '../../types/youtube-player';
|
import { YoutubePlayer } from '../../types/youtube-player';
|
||||||
|
|
||||||
function $(selector: string): HTMLElement | null {
|
// export default () => {
|
||||||
return document.querySelector(selector);
|
// document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
||||||
}
|
// };
|
||||||
|
|
||||||
const qualitySettingsButton = ElementFromHtml(qualitySettingsTemplate);
|
export default builder.createRenderer(({ invoke }) => {
|
||||||
|
function $(selector: string): HTMLElement | null {
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
function setup(event: CustomEvent<YoutubePlayer>) {
|
const qualitySettingsButton = ElementFromHtml(qualitySettingsTemplate);
|
||||||
const api = event.detail;
|
|
||||||
|
|
||||||
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
|
function setup(event: CustomEvent<YoutubePlayer>) {
|
||||||
|
const api = event.detail;
|
||||||
|
|
||||||
qualitySettingsButton.addEventListener('click', function chooseQuality() {
|
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
|
||||||
setTimeout(() => $('#player')?.click());
|
|
||||||
|
|
||||||
const qualityLevels = api.getAvailableQualityLevels();
|
qualitySettingsButton.addEventListener('click', function chooseQuality() {
|
||||||
|
setTimeout(() => $('#player')?.click());
|
||||||
|
|
||||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
const qualityLevels = api.getAvailableQualityLevels();
|
||||||
|
|
||||||
window.ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => {
|
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||||
if (promise.response === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newQuality = qualityLevels[promise.response];
|
invoke<{ response: number }>('qualityChanger', api.getAvailableQualityLabels(), currentIndex)
|
||||||
api.setPlaybackQualityRange(newQuality);
|
.then((promise) => {
|
||||||
api.setPlaybackQuality(newQuality);
|
if (promise.response === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newQuality = qualityLevels[promise.response];
|
||||||
|
api.setPlaybackQualityRange(newQuality);
|
||||||
|
api.setPlaybackQuality(newQuality);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
return {
|
||||||
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
onLoad() {
|
||||||
};
|
console.log('qc');
|
||||||
|
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -19,20 +19,33 @@ export type PreloadPlugin<Config extends PluginBaseConfig> = BasePlugin<Config>;
|
|||||||
type DeepPartial<T> = {
|
type DeepPartial<T> = {
|
||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
};
|
};
|
||||||
export type PluginContext<Config extends PluginBaseConfig> = {
|
type IF<T> = (args: T) => T;
|
||||||
getConfig: () => Config;
|
type Promisable<T> = T | Promise<T>;
|
||||||
setConfig: (config: DeepPartial<Config>) => void;
|
|
||||||
|
|
||||||
send: (event: string, ...args: unknown[]) => void;
|
export type PluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = {
|
||||||
on: (event: string, listener: (...args: unknown[]) => void) => void;
|
getConfig: () => Promise<Config>;
|
||||||
|
setConfig: (config: DeepPartial<Config>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IF<T> = (args: T) => T;
|
export type MainPluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = PluginContext<Config> & {
|
||||||
|
send: (event: string, ...args: unknown[]) => void;
|
||||||
|
handle: <Arguments extends unknown[], Return>(event: string, listener: (...args: Arguments) => Promisable<Return>) => void;
|
||||||
|
};
|
||||||
|
export type RendererPluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = PluginContext<Config> & {
|
||||||
|
invoke: <Return>(event: string, ...args: unknown[]) => Promise<Return>;
|
||||||
|
on: <Arguments extends unknown[]>(event: string, listener: (...args: Arguments) => Promisable<void>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 PluginBuilder<ID extends string, Config extends PluginBaseConfig> = {
|
export type PluginBuilder<ID extends string, Config extends PluginBaseConfig> = {
|
||||||
createRenderer: IF<(context: PluginContext<Config>) => RendererPlugin<Config>>;
|
createRenderer: IF<RendererPluginFactory<Config>>;
|
||||||
createMain: IF<(context: PluginContext<Config>) => MainPlugin<Config>>;
|
createMain: IF<MainPluginFactory<Config>>;
|
||||||
createPreload: IF<(context: PluginContext<Config>) => PreloadPlugin<Config>>;
|
createPreload: IF<PreloadPluginFactory<Config>>;
|
||||||
createMenu: IF<(context: PluginContext<Config>) => MenuItemConstructorOptions[]>;
|
createMenu: IF<MenuPluginFactory<Config>>;
|
||||||
|
|
||||||
id: ID;
|
id: ID;
|
||||||
config: Config;
|
config: Config;
|
||||||
@ -48,7 +61,7 @@ export type PluginBuilderOptions<Config extends PluginBaseConfig = PluginBaseCon
|
|||||||
export const createPluginBuilder = <ID extends string, Config extends PluginBaseConfig>(
|
export const createPluginBuilder = <ID extends string, Config extends PluginBaseConfig>(
|
||||||
id: ID,
|
id: ID,
|
||||||
options: PluginBuilderOptions<Config>,
|
options: PluginBuilderOptions<Config>,
|
||||||
): PluginBuilder<ID, Config> => ({
|
): PluginBuilder<ID, Omit<Config, 'enabled'> & PluginBaseConfig> => ({
|
||||||
createRenderer: (plugin) => plugin,
|
createRenderer: (plugin) => plugin,
|
||||||
createMain: (plugin) => plugin,
|
createMain: (plugin) => plugin,
|
||||||
createPreload: (plugin) => plugin,
|
createPreload: (plugin) => plugin,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import setupSongInfo from './providers/song-info-front';
|
|
||||||
import { setupSongControls } from './providers/song-controls-front';
|
|
||||||
import { startingPages } from './providers/extracted-data';
|
|
||||||
|
|
||||||
// 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 { startingPages } from './providers/extracted-data';
|
||||||
|
import { setupSongControls } from './providers/song-controls-front';
|
||||||
|
import setupSongInfo from './providers/song-info-front';
|
||||||
|
|
||||||
const enabledPluginNameAndOptions = window.mainConfig.plugins.getEnabled();
|
const enabledPluginNameAndOptions = window.mainConfig.plugins.getEnabled();
|
||||||
|
|
||||||
let api: Element | null = null;
|
let api: Element | null = null;
|
||||||
@ -91,8 +94,41 @@ function onApiLoaded() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(() => {
|
const createContext = <
|
||||||
|
Key extends keyof PluginBuilderList,
|
||||||
|
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
||||||
|
>(name: Key): RendererPluginContext<Config> => ({
|
||||||
|
getConfig: async () => {
|
||||||
|
return await window.ipcRenderer.invoke('get-config', name) as Config;
|
||||||
|
},
|
||||||
|
setConfig: async (newConfig) => {
|
||||||
|
await window.ipcRenderer.invoke('set-config', name, newConfig);
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: async <Return>(event: string, ...args: unknown[]): Promise<Return> => {
|
||||||
|
return await window.ipcRenderer.invoke(event, ...args) as Return;
|
||||||
|
},
|
||||||
|
on: (event: string, listener) => {
|
||||||
|
window.ipcRenderer.on(event, async (_, ...args) => listener(...args as never));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => {
|
enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => {
|
||||||
|
if (pluginName === 'ambient-mode') {
|
||||||
|
const builder = rendererPlugins[pluginName];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = createContext(pluginName);
|
||||||
|
const plugin = await builder?.(context);
|
||||||
|
console.log(plugin);
|
||||||
|
plugin.onLoad?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in plugin "${pluginName}"`);
|
||||||
|
console.trace(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.hasOwn(rendererPlugins, pluginName)) {
|
if (Object.hasOwn(rendererPlugins, pluginName)) {
|
||||||
const handler = rendererPlugins[pluginName];
|
const handler = rendererPlugins[pluginName];
|
||||||
try {
|
try {
|
||||||
@ -103,6 +139,22 @@ function onApiLoaded() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const rendererPluginList = await Promise.all(
|
||||||
|
newPluginList.map(async ([id, plugin]) => {
|
||||||
|
const context = createContext(id);
|
||||||
|
return [id, await plugin(context)] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
rendererPluginList.forEach(([, plugin]) => plugin.onLoad?.());
|
||||||
|
|
||||||
|
window.ipcRenderer.on('config-changed', (_event, id: string, newConfig) => {
|
||||||
|
const plugin = rendererPluginList.find(([pluginId]) => pluginId === id);
|
||||||
|
|
||||||
|
if (plugin) {
|
||||||
|
plugin[1].onConfigChange?.(newConfig as never);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Inject song-info provider
|
// Inject song-info provider
|
||||||
setupSongInfo();
|
setupSongInfo();
|
||||||
|
|||||||
22
src/virtual-module.d.ts
vendored
22
src/virtual-module.d.ts
vendored
@ -1,26 +1,24 @@
|
|||||||
declare module 'virtual:MainPlugins' {
|
|
||||||
import type { BrowserWindow } from 'electron';
|
|
||||||
import type { ConfigType } from './config/dynamic';
|
|
||||||
|
|
||||||
export const mainPlugins: Record<string, (win: BrowserWindow, options: ConfigType) => Promise<void>>;
|
declare module 'virtual:MainPlugins' {
|
||||||
|
import type { MainPluginFactory } from './plugins/utils/builder';
|
||||||
|
|
||||||
|
export const mainPlugins: Record<string, MainPluginFactory>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'virtual:MenuPlugins' {
|
declare module 'virtual:MenuPlugins' {
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { MenuPluginFactory } from './plugins/utils/builder';
|
||||||
import type { MenuTemplate } from './menu';
|
|
||||||
import type { ConfigType } from './config/dynamic';
|
|
||||||
|
|
||||||
export const menuPlugins: Record<string, (win: BrowserWindow, options: ConfigType, refreshMenu: () => void) => MenuTemplate>;
|
export const menuPlugins: Record<string, MenuPluginFactory>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'virtual:PreloadPlugins' {
|
declare module 'virtual:PreloadPlugins' {
|
||||||
import type { ConfigType } from './config/dynamic';
|
import type { PreloadPluginFactory } from './plugins/utils/builder';
|
||||||
|
|
||||||
export const preloadPlugins: Record<string, (options: ConfigType) => Promise<void>>;
|
export const preloadPlugins: Record<string, PreloadPluginFactory>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'virtual:RendererPlugins' {
|
declare module 'virtual:RendererPlugins' {
|
||||||
import type { ConfigType } from './config/dynamic';
|
import type { RendererPluginFactory } from './plugins/utils/builder';
|
||||||
|
|
||||||
export const rendererPlugins: Record<string, (options: ConfigType) => Promise<void>>;
|
export const rendererPlugins: Record<string, RendererPluginFactory>;
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/youtube-music.d.ts
vendored
6
src/youtube-music.d.ts
vendored
@ -29,3 +29,9 @@ declare module '*.css' {
|
|||||||
|
|
||||||
export default css;
|
export default css;
|
||||||
}
|
}
|
||||||
|
declare module '*.css?inline' {
|
||||||
|
const css: string;
|
||||||
|
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,14 @@ export const pluginVirtualModuleGenerator = (mode: 'main' | 'preload' | 'rendere
|
|||||||
|
|
||||||
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 }) => !name.startsWith('utils') && existsSync(resolve(path, `${mode}.ts`)));
|
.filter(({ name, path }) => {
|
||||||
|
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`));
|
||||||
|
});
|
||||||
|
// for test !name.startsWith('ambient-mode')
|
||||||
|
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user