feat(plugin): migrating plugins to new plugin system (WIP)

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Su-Yong
2023-11-10 21:32:05 +09:00
parent afe6accab8
commit e0e17cac99
18 changed files with 538 additions and 273 deletions

View 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;
}
}

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

@ -1,127 +1,130 @@
import { FastAverageColor } from 'fast-average-color';
import type { ConfigType } from '../../config/dynamic';
import builder from './';
function hexToHSL(H: string) {
// Convert hex to RGB first
let r = 0;
let g = 0;
let b = 0;
if (H.length == 4) {
r = Number('0x' + H[1] + H[1]);
g = Number('0x' + H[2] + H[2]);
b = Number('0x' + H[3] + H[3]);
} else if (H.length == 7) {
r = Number('0x' + H[1] + H[2]);
g = Number('0x' + H[3] + H[4]);
b = Number('0x' + H[5] + H[6]);
export default builder.createRenderer(() => {
function hexToHSL(H: string) {
// Convert hex to RGB first
let r = 0;
let g = 0;
let b = 0;
if (H.length == 4) {
r = Number('0x' + H[1] + H[1]);
g = Number('0x' + H[2] + H[2]);
b = Number('0x' + H[3] + H[3]);
} else if (H.length == 7) {
r = Number('0x' + H[1] + H[2]);
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;
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;
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}%)`;
}
}
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);
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];
}
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';
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));
}
}
});
});
};
};
});

View File

@ -1,4 +1,4 @@
import style from './style.css';
import style from './style.css?inline';
import { createPluginBuilder } from '../utils/builder';

View File

@ -7,79 +7,83 @@ const bufferList = [1, 5, 10, 20, 30];
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];
export default builder.createMenu(({ getConfig, setConfig }) => ([
{
label: 'Smoothness transition',
submenu: interpolationTimeList.map((interpolationTime) => ({
label: `During ${interpolationTime / 1000}s`,
type: 'radio',
checked: getConfig().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 });
export default builder.createMenu(async ({ getConfig, setConfig }) => {
const config = await getConfig();
return [
{
label: 'Smoothness transition',
submenu: interpolationTimeList.map((interpolationTime) => ({
label: `During ${interpolationTime / 1000}s`,
type: 'radio',
checked: config.interpolationTime === interpolationTime,
click() {
setConfig({ interpolationTime });
},
})),
},
},
]));
{
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 });
},
},
];
});

View File

@ -1,7 +1,7 @@
import builder from './index';
export default builder.createRenderer(({ getConfig }) => {
const initConfigData = getConfig();
export default builder.createRenderer(async ({ getConfig }) => {
const initConfigData = await getConfig();
let interpolationTime = initConfigData.interpolationTime;
let buffer = initConfigData.buffer;
@ -26,6 +26,7 @@ export default builder.createRenderer(({ getConfig }) => {
if (!video) return null;
if (!wrapper) return null;
console.log('injectBlurVideo', songVideo, video, wrapper);
const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas');
@ -39,6 +40,7 @@ export default builder.createRenderer(({ getConfig }) => {
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => {
// console.log('context', context);
if (!context) return;
const width = qualityRatio;

View 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;
}
}

View File

@ -1,13 +1,17 @@
import { ipcMain, dialog, BrowserWindow } from 'electron';
import { dialog, BrowserWindow } from 'electron';
export default (win: BrowserWindow) => {
ipcMain.handle('qualityChanger', async (_, qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(win, {
type: 'question',
buttons: qualityLabels,
defaultId: currentIndex,
title: 'Choose Video Quality',
message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1,
}));
};
import builder from './index';
export default builder.createMain(({ handle }) => ({
onLoad(win: BrowserWindow) {
handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(win, {
type: 'question',
buttons: qualityLabels,
defaultId: currentIndex,
title: 'Choose Video Quality',
message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1,
}));
}
}));

View File

@ -1,38 +1,50 @@
import qualitySettingsTemplate from './templates/qualitySettingsTemplate.html?raw';
import builder from './';
import { ElementFromHtml } from '../utils/renderer';
import { YoutubePlayer } from '../../types/youtube-player';
function $(selector: string): HTMLElement | null {
return document.querySelector(selector);
}
// export default () => {
// 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 api = event.detail;
const qualitySettingsButton = ElementFromHtml(qualitySettingsTemplate);
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
function setup(event: CustomEvent<YoutubePlayer>) {
const api = event.detail;
qualitySettingsButton.addEventListener('click', function chooseQuality() {
setTimeout(() => $('#player')?.click());
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
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 }) => {
if (promise.response === -1) {
return;
}
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality);
invoke<{ response: number }>('qualityChanger', api.getAvailableQualityLabels(), currentIndex)
.then((promise) => {
if (promise.response === -1) {
return;
}
const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality);
});
});
});
}
}
export default () => {
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
};
return {
onLoad() {
console.log('qc');
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
}
};
});

View File

@ -19,20 +19,33 @@ export type PreloadPlugin<Config extends PluginBaseConfig> = BasePlugin<Config>;
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
export type PluginContext<Config extends PluginBaseConfig> = {
getConfig: () => Config;
setConfig: (config: DeepPartial<Config>) => void;
type IF<T> = (args: T) => T;
type Promisable<T> = T | Promise<T>;
send: (event: string, ...args: unknown[]) => void;
on: (event: string, listener: (...args: unknown[]) => void) => void;
export type PluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = {
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> = {
createRenderer: IF<(context: PluginContext<Config>) => RendererPlugin<Config>>;
createMain: IF<(context: PluginContext<Config>) => MainPlugin<Config>>;
createPreload: IF<(context: PluginContext<Config>) => PreloadPlugin<Config>>;
createMenu: IF<(context: PluginContext<Config>) => MenuItemConstructorOptions[]>;
createRenderer: IF<RendererPluginFactory<Config>>;
createMain: IF<MainPluginFactory<Config>>;
createPreload: IF<PreloadPluginFactory<Config>>;
createMenu: IF<MenuPluginFactory<Config>>;
id: ID;
config: Config;
@ -48,7 +61,7 @@ export type PluginBuilderOptions<Config extends PluginBaseConfig = PluginBaseCon
export const createPluginBuilder = <ID extends string, Config extends PluginBaseConfig>(
id: ID,
options: PluginBuilderOptions<Config>,
): PluginBuilder<ID, Config> => ({
): PluginBuilder<ID, Omit<Config, 'enabled'> & PluginBaseConfig> => ({
createRenderer: (plugin) => plugin,
createMain: (plugin) => plugin,
createPreload: (plugin) => plugin,

View File

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