mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
@ -4,13 +4,13 @@ import { rendererPlugins } from 'virtual:plugins';
|
||||
|
||||
import { RendererContext } from '@/types/contexts';
|
||||
|
||||
import { PluginDef } from '@/types/plugins';
|
||||
import { PluginConfig, PluginDef } from '@/types/plugins';
|
||||
import { startPlugin, stopPlugin } from '@/utils';
|
||||
|
||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
||||
const loadedPluginMap: Record<string, PluginDef> = {};
|
||||
const loadedPluginMap: Record<string, PluginDef<unknown, unknown, unknown, PluginConfig>> = {};
|
||||
|
||||
const createContext = (id: string): RendererContext => ({
|
||||
const createContext = <Config extends PluginConfig>(id: string): RendererContext<Config> => ({
|
||||
getConfig: () => window.mainConfig.plugins.getOptions(id),
|
||||
setConfig: async (newConfig) => {
|
||||
await window.ipcRenderer.invoke('set-config', id, newConfig);
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { blockers } from './types';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from '@/plugins/adblocker/blocker';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import injectCliqzPreload from '@/plugins/adblocker/injectors/inject-cliqz-preload';
|
||||
import { inject, isInjected } from '@/plugins/adblocker/injectors/inject';
|
||||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
interface AdblockerConfig {
|
||||
/**
|
||||
@ -31,7 +36,7 @@ interface AdblockerConfig {
|
||||
disableDefaultLists: boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('adblocker', {
|
||||
export default createPlugin({
|
||||
name: 'Adblocker',
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
@ -41,12 +46,76 @@ const builder = createPluginBuilder('adblocker', {
|
||||
additionalBlockLists: [],
|
||||
disableDefaultLists: false,
|
||||
} as AdblockerConfig,
|
||||
});
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
export default builder;
|
||||
return [
|
||||
{
|
||||
label: 'Blocker',
|
||||
submenu: Object.values(blockers).map((blocker) => ({
|
||||
label: blocker,
|
||||
type: 'radio',
|
||||
checked: (config.blocker || blockers.WithBlocklists) === blocker,
|
||||
click() {
|
||||
setConfig({ blocker });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
mainWindow: null as BrowserWindow | null,
|
||||
async start({ getConfig, window }) {
|
||||
const config = await getConfig();
|
||||
this.mainWindow = window;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
await loadAdBlockerEngine(
|
||||
window.webContents.session,
|
||||
config.cache,
|
||||
config.additionalBlockLists,
|
||||
config.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
},
|
||||
stop({ window }) {
|
||||
if (isBlockerEnabled(window.webContents.session)) {
|
||||
unloadAdBlockerEngine(window.webContents.session);
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
console.log('Adblocker config changed', newConfig);
|
||||
if (this.mainWindow) {
|
||||
if (newConfig.blocker === blockers.WithBlocklists && !isBlockerEnabled(this.mainWindow.webContents.session)) {
|
||||
await loadAdBlockerEngine(
|
||||
this.mainWindow.webContents.session,
|
||||
newConfig.cache,
|
||||
newConfig.additionalBlockLists,
|
||||
newConfig.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
await injectCliqzPreload();
|
||||
} else if (config.blocker === blockers.InPlayer) {
|
||||
inject();
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (newConfig.blocker === blockers.WithBlocklists) {
|
||||
await injectCliqzPreload();
|
||||
} else if (newConfig.blocker === blockers.InPlayer) {
|
||||
if (!isInjected()) {
|
||||
inject();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from './blocker';
|
||||
|
||||
import builder from './index';
|
||||
import { blockers } from './types';
|
||||
|
||||
export default builder.createMain(({ getConfig }) => {
|
||||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
return {
|
||||
async onLoad(window) {
|
||||
const config = await getConfig();
|
||||
mainWindow = window;
|
||||
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
await loadAdBlockerEngine(
|
||||
window.webContents.session,
|
||||
config.cache,
|
||||
config.additionalBlockLists,
|
||||
config.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUnload(window) {
|
||||
if (isBlockerEnabled(window.webContents.session)) {
|
||||
unloadAdBlockerEngine(window.webContents.session);
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (mainWindow) {
|
||||
if (newConfig.blocker === blockers.WithBlocklists && !isBlockerEnabled(mainWindow.webContents.session)) {
|
||||
await loadAdBlockerEngine(
|
||||
mainWindow.webContents.session,
|
||||
newConfig.cache,
|
||||
newConfig.additionalBlockLists,
|
||||
newConfig.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import { blockers } from './types';
|
||||
import builder from './index';
|
||||
|
||||
import type { MenuTemplate } from '../../menu';
|
||||
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }): Promise<MenuTemplate> => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Blocker',
|
||||
submenu: Object.values(blockers).map((blocker) => ({
|
||||
label: blocker,
|
||||
type: 'radio',
|
||||
checked: (config.blocker || blockers.WithBlocklists) === blocker,
|
||||
click() {
|
||||
setConfig({ blocker });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,27 +0,0 @@
|
||||
import { inject, isInjected } from './injectors/inject';
|
||||
import injectCliqzPreload from './injectors/inject-cliqz-preload';
|
||||
|
||||
import { blockers } from './types';
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createPreload(({ getConfig }) => ({
|
||||
async onLoad() {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
await injectCliqzPreload();
|
||||
} else if (config.blocker === blockers.InPlayer) {
|
||||
inject();
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (newConfig.blocker === blockers.WithBlocklists) {
|
||||
await injectCliqzPreload();
|
||||
} else if (newConfig.blocker === blockers.InPlayer) {
|
||||
if (!isInjected()) {
|
||||
inject();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
@ -1,20 +1,144 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
const builder = createPluginBuilder('album-color-theme', {
|
||||
export default createPlugin({
|
||||
name: 'Album Color Theme',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
styles: [style],
|
||||
});
|
||||
stylesheets: [style],
|
||||
renderer: {
|
||||
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;
|
||||
|
||||
export default builder;
|
||||
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;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
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];
|
||||
},
|
||||
hue: 0,
|
||||
saturation: 0,
|
||||
lightness: 0,
|
||||
|
||||
changeElementColor: (element: HTMLElement | null, hue: number, saturation: number, lightness: number) => {
|
||||
if (element) {
|
||||
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
}
|
||||
},
|
||||
|
||||
playerPage: null as HTMLElement | null,
|
||||
navBarBackground: null as HTMLElement | null,
|
||||
ytmusicPlayerBar: null as HTMLElement | null,
|
||||
playerBarBackground: null as HTMLElement | null,
|
||||
sidebarBig: null as HTMLElement | null,
|
||||
sidebarSmall: null as HTMLElement | null,
|
||||
ytmusicAppLayout: null as HTMLElement | null,
|
||||
|
||||
start() {
|
||||
this.playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
this.navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||
this.ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||
this.playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||
this.sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||
this.sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen = this.ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.changeElementColor(this.sidebarSmall, this.hue, this.saturation, this.lightness - 30);
|
||||
} else {
|
||||
if (this.sidebarSmall) {
|
||||
this.sidebarSmall.style.backgroundColor = 'black';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.playerPage) {
|
||||
observer.observe(this.playerPage, { attributes: true });
|
||||
}
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
const fastAverageColor = new FastAverageColor();
|
||||
|
||||
playerApi.addEventListener('videodatachange', (name: string) => {
|
||||
if (name === 'dataloaded') {
|
||||
const playerResponse = playerApi.getPlayerResponse();
|
||||
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||
if (thumbnail) {
|
||||
fastAverageColor.getColorAsync(thumbnail.url)
|
||||
.then((albumColor) => {
|
||||
if (albumColor) {
|
||||
const [hue, saturation, lightness] = this.hexToHSL(albumColor.hex);
|
||||
this.changeElementColor(this.playerPage, hue, saturation, lightness - 30);
|
||||
this.changeElementColor(this.navBarBackground, hue, saturation, lightness - 15);
|
||||
this.changeElementColor(this.ytmusicPlayerBar, hue, saturation, lightness - 15);
|
||||
this.changeElementColor(this.playerBarBackground, hue, saturation, lightness - 15);
|
||||
this.changeElementColor(this.sidebarBig, hue, saturation, lightness - 15);
|
||||
if (this.ytmusicAppLayout?.hasAttribute('player-page-open')) {
|
||||
this.changeElementColor(this.sidebarSmall, hue, saturation, lightness - 30);
|
||||
}
|
||||
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
|
||||
this.changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
|
||||
} else {
|
||||
if (this.playerPage) {
|
||||
this.playerPage.style.backgroundColor = '#000000';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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}%)`;
|
||||
}
|
||||
}
|
||||
|
||||
let playerPage: HTMLElement | null = null;
|
||||
let navBarBackground: HTMLElement | null = null;
|
||||
let ytmusicPlayerBar: HTMLElement | null = null;
|
||||
let playerBarBackground: HTMLElement | null = null;
|
||||
let sidebarBig: HTMLElement | null = null;
|
||||
let sidebarSmall: HTMLElement | null = null;
|
||||
let ytmusicAppLayout: HTMLElement | null = null;
|
||||
|
||||
return {
|
||||
onLoad() {
|
||||
playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||
ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||
playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||
sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||
sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||
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 });
|
||||
}
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
const fastAverageColor = new FastAverageColor();
|
||||
|
||||
playerApi.addEventListener('videodatachange', (name: string) => {
|
||||
if (name === 'dataloaded') {
|
||||
const playerResponse = playerApi.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,6 +1,6 @@
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export type AmbientModePluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -12,26 +12,286 @@ export type AmbientModePluginConfig = {
|
||||
opacity: number;
|
||||
fullscreen: boolean;
|
||||
};
|
||||
const builder = createPluginBuilder('ambient-mode', {
|
||||
const defaultConfig: AmbientModePluginConfig = {
|
||||
enabled: false,
|
||||
quality: 50,
|
||||
buffer: 30,
|
||||
interpolationTime: 1500,
|
||||
blur: 100,
|
||||
size: 100,
|
||||
opacity: 1,
|
||||
fullscreen: false,
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Ambient Mode',
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
quality: 50,
|
||||
buffer: 30,
|
||||
interpolationTime: 1500,
|
||||
blur: 100,
|
||||
size: 100,
|
||||
opacity: 1,
|
||||
fullscreen: false,
|
||||
} as AmbientModePluginConfig,
|
||||
styles: [style],
|
||||
});
|
||||
config: defaultConfig,
|
||||
stylesheets: [style],
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
|
||||
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
|
||||
const sizeList = [100, 110, 125, 150, 175, 200, 300];
|
||||
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;
|
||||
const config = await getConfig();
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
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 });
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderer: {
|
||||
interpolationTime: defaultConfig.interpolationTime,
|
||||
buffer: defaultConfig.buffer,
|
||||
qualityRatio: defaultConfig.quality,
|
||||
sizeRatio: defaultConfig.size / 100,
|
||||
blur: defaultConfig.blur,
|
||||
opacity: defaultConfig.opacity,
|
||||
isFullscreen: defaultConfig.fullscreen,
|
||||
|
||||
unregister: null as (() => void) | null,
|
||||
update: null as (() => void) | null,
|
||||
observer: null as MutationObserver | null,
|
||||
|
||||
start() {
|
||||
const injectBlurVideo = (): (() => void) | null => {
|
||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||
const video = document.querySelector<HTMLVideoElement>('#song-video .html5-video-container > video');
|
||||
const wrapper = document.querySelector('#song-video > .player-wrapper');
|
||||
|
||||
if (!songVideo) return null;
|
||||
if (!video) return null;
|
||||
if (!wrapper) return null;
|
||||
|
||||
const blurCanvas = document.createElement('canvas');
|
||||
blurCanvas.classList.add('html5-blur-canvas');
|
||||
|
||||
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
/* effect */
|
||||
let lastEffectWorkId: number | null = null;
|
||||
let lastImageData: ImageData | null = null;
|
||||
|
||||
const onSync = () => {
|
||||
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
|
||||
|
||||
lastEffectWorkId = requestAnimationFrame(() => {
|
||||
// console.log('context', context);
|
||||
if (!context) return;
|
||||
|
||||
const width = this.qualityRatio;
|
||||
let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1);
|
||||
if (!Number.isFinite(height)) height = width;
|
||||
if (!height) return;
|
||||
|
||||
context.globalAlpha = 1;
|
||||
if (lastImageData) {
|
||||
const frameOffset = (1 / this.buffer) * (1000 / this.interpolationTime);
|
||||
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
|
||||
context.putImageData(lastImageData, 0, 0);
|
||||
context.globalAlpha = frameOffset;
|
||||
}
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
lastImageData = context.getImageData(0, 0, width, height); // current image data
|
||||
|
||||
lastEffectWorkId = null;
|
||||
});
|
||||
};
|
||||
|
||||
const applyVideoAttributes = () => {
|
||||
const rect = video.getBoundingClientRect();
|
||||
|
||||
const newWidth = Math.floor(video.width || rect.width);
|
||||
const newHeight = Math.floor(video.height || rect.height);
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
blurCanvas.width = this.qualityRatio;
|
||||
blurCanvas.height = Math.floor(newHeight / newWidth * this.qualityRatio);
|
||||
blurCanvas.style.width = `${newWidth * this.sizeRatio}px`;
|
||||
blurCanvas.style.height = `${newHeight * this.sizeRatio}px`;
|
||||
|
||||
if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
|
||||
else blurCanvas.classList.remove('fullscreen');
|
||||
|
||||
const leftOffset = newWidth * (this.sizeRatio - 1) / 2;
|
||||
const topOffset = newHeight * (this.sizeRatio - 1) / 2;
|
||||
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
|
||||
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
|
||||
blurCanvas.style.setProperty('--blur', `${this.blur}px`);
|
||||
blurCanvas.style.setProperty('--opacity', `${this.opacity}`);
|
||||
};
|
||||
this.update = applyVideoAttributes;
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
applyVideoAttributes();
|
||||
}
|
||||
});
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
applyVideoAttributes();
|
||||
});
|
||||
|
||||
/* hooking */
|
||||
let canvasInterval: NodeJS.Timeout | null = null;
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer)));
|
||||
applyVideoAttributes();
|
||||
observer.observe(songVideo, { attributes: true });
|
||||
resizeObserver.observe(songVideo);
|
||||
window.addEventListener('resize', applyVideoAttributes);
|
||||
|
||||
const onPause = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = null;
|
||||
};
|
||||
const onPlay = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer)));
|
||||
};
|
||||
songVideo.addEventListener('pause', onPause);
|
||||
songVideo.addEventListener('play', onPlay);
|
||||
|
||||
/* injecting */
|
||||
wrapper.prepend(blurCanvas);
|
||||
|
||||
/* cleanup */
|
||||
return () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
|
||||
songVideo.removeEventListener('pause', onPause);
|
||||
songVideo.removeEventListener('play', onPlay);
|
||||
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', applyVideoAttributes);
|
||||
|
||||
wrapper.removeChild(blurCanvas);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.unregister?.();
|
||||
this.unregister = injectBlurVideo() ?? null;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.unregister?.();
|
||||
this.unregister = injectBlurVideo() ?? null;
|
||||
} else {
|
||||
this.unregister?.();
|
||||
this.unregister = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (playerPage) {
|
||||
observer.observe(playerPage, { attributes: true });
|
||||
}
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
this.interpolationTime = newConfig.interpolationTime;
|
||||
this.buffer = newConfig.buffer;
|
||||
this.qualityRatio = newConfig.quality;
|
||||
this.sizeRatio = newConfig.size / 100;
|
||||
this.blur = newConfig.blur;
|
||||
this.opacity = newConfig.opacity;
|
||||
this.isFullscreen = newConfig.fullscreen;
|
||||
|
||||
this.update?.();
|
||||
},
|
||||
stop() {
|
||||
this.observer?.disconnect();
|
||||
this.update = null;
|
||||
this.unregister?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
|
||||
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
|
||||
const sizeList = [100, 110, 125, 150, 175, 200, 300];
|
||||
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(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 });
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -1,184 +0,0 @@
|
||||
import builder from './index';
|
||||
|
||||
export default builder.createRenderer(async ({ getConfig }) => {
|
||||
const initConfigData = await getConfig();
|
||||
|
||||
let interpolationTime = initConfigData.interpolationTime;
|
||||
let buffer = initConfigData.buffer;
|
||||
let qualityRatio =initConfigData.quality;
|
||||
let sizeRatio = initConfigData.size / 100;
|
||||
let blur = initConfigData.blur;
|
||||
let opacity = initConfigData.opacity;
|
||||
let isFullscreen = initConfigData.fullscreen;
|
||||
|
||||
let unregister: (() => void) | null = null;
|
||||
let update: (() => void) | null = null;
|
||||
let observer: MutationObserver;
|
||||
|
||||
return {
|
||||
onLoad() {
|
||||
const injectBlurVideo = (): (() => void) | null => {
|
||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||
const video = document.querySelector<HTMLVideoElement>('#song-video .html5-video-container > video');
|
||||
const wrapper = document.querySelector('#song-video > .player-wrapper');
|
||||
|
||||
if (!songVideo) return null;
|
||||
if (!video) return null;
|
||||
if (!wrapper) return null;
|
||||
|
||||
const blurCanvas = document.createElement('canvas');
|
||||
blurCanvas.classList.add('html5-blur-canvas');
|
||||
|
||||
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
/* effect */
|
||||
let lastEffectWorkId: number | null = null;
|
||||
let lastImageData: ImageData | null = null;
|
||||
|
||||
const onSync = () => {
|
||||
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
|
||||
|
||||
lastEffectWorkId = requestAnimationFrame(() => {
|
||||
// console.log('context', context);
|
||||
if (!context) return;
|
||||
|
||||
const width = qualityRatio;
|
||||
let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1);
|
||||
if (!Number.isFinite(height)) height = width;
|
||||
if (!height) return;
|
||||
|
||||
context.globalAlpha = 1;
|
||||
if (lastImageData) {
|
||||
const frameOffset = (1 / buffer) * (1000 / interpolationTime);
|
||||
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
|
||||
context.putImageData(lastImageData, 0, 0);
|
||||
context.globalAlpha = frameOffset;
|
||||
}
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
lastImageData = context.getImageData(0, 0, width, height); // current image data
|
||||
|
||||
lastEffectWorkId = null;
|
||||
});
|
||||
};
|
||||
|
||||
const applyVideoAttributes = () => {
|
||||
const rect = video.getBoundingClientRect();
|
||||
|
||||
const newWidth = Math.floor(video.width || rect.width);
|
||||
const newHeight = Math.floor(video.height || rect.height);
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
blurCanvas.width = qualityRatio;
|
||||
blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio);
|
||||
blurCanvas.style.width = `${newWidth * sizeRatio}px`;
|
||||
blurCanvas.style.height = `${newHeight * sizeRatio}px`;
|
||||
|
||||
if (isFullscreen) blurCanvas.classList.add('fullscreen');
|
||||
else blurCanvas.classList.remove('fullscreen');
|
||||
|
||||
const leftOffset = newWidth * (sizeRatio - 1) / 2;
|
||||
const topOffset = newHeight * (sizeRatio - 1) / 2;
|
||||
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
|
||||
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
|
||||
blurCanvas.style.setProperty('--blur', `${blur}px`);
|
||||
blurCanvas.style.setProperty('--opacity', `${opacity}`);
|
||||
};
|
||||
update = applyVideoAttributes;
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
applyVideoAttributes();
|
||||
}
|
||||
});
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
applyVideoAttributes();
|
||||
});
|
||||
|
||||
/* hooking */
|
||||
let canvasInterval: NodeJS.Timeout | null = null;
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
|
||||
applyVideoAttributes();
|
||||
observer.observe(songVideo, { attributes: true });
|
||||
resizeObserver.observe(songVideo);
|
||||
window.addEventListener('resize', applyVideoAttributes);
|
||||
|
||||
const onPause = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = null;
|
||||
};
|
||||
const onPlay = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
|
||||
};
|
||||
songVideo.addEventListener('pause', onPause);
|
||||
songVideo.addEventListener('play', onPlay);
|
||||
|
||||
/* injecting */
|
||||
wrapper.prepend(blurCanvas);
|
||||
|
||||
/* cleanup */
|
||||
return () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
|
||||
songVideo.removeEventListener('pause', onPause);
|
||||
songVideo.removeEventListener('play', onPlay);
|
||||
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', applyVideoAttributes);
|
||||
|
||||
wrapper.removeChild(blurCanvas);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
unregister?.();
|
||||
unregister = injectBlurVideo() ?? null;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
unregister?.();
|
||||
unregister = injectBlurVideo() ?? null;
|
||||
} else {
|
||||
unregister?.();
|
||||
unregister = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (playerPage) {
|
||||
observer.observe(playerPage, { attributes: true });
|
||||
}
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
interpolationTime = newConfig.interpolationTime;
|
||||
buffer = newConfig.buffer;
|
||||
qualityRatio = newConfig.quality;
|
||||
sizeRatio = newConfig.size / 100;
|
||||
blur = newConfig.blur;
|
||||
opacity = newConfig.opacity;
|
||||
isFullscreen = newConfig.fullscreen;
|
||||
|
||||
update?.();
|
||||
},
|
||||
onUnload() {
|
||||
observer?.disconnect();
|
||||
update = null;
|
||||
unregister?.();
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -3,5 +3,6 @@ import style from './style.css?inline';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Blur Navigation Bar',
|
||||
renderer: { stylesheets: [style] },
|
||||
stylesheets: [style],
|
||||
renderer() {},
|
||||
});
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import type {
|
||||
BrowserWindow,
|
||||
MenuItemConstructorOptions,
|
||||
} from 'electron';
|
||||
import type { YoutubePlayer } from '../../types/youtube-player';
|
||||
|
||||
export type PluginBaseConfig = {
|
||||
enabled: boolean;
|
||||
};
|
||||
export type BasePlugin<Config extends PluginBaseConfig> = {
|
||||
onLoad?: () => void;
|
||||
onUnload?: () => void;
|
||||
onConfigChange?: (newConfig: Config) => void;
|
||||
}
|
||||
export type RendererPlugin<Config extends PluginBaseConfig> = BasePlugin<Config> & {
|
||||
onPlayerApiReady?: (api: YoutubePlayer) => void;
|
||||
};
|
||||
export type MainPlugin<Config extends PluginBaseConfig> = Omit<BasePlugin<Config>, 'onLoad' | 'onUnload'> & {
|
||||
onLoad?: (window: BrowserWindow) => void;
|
||||
onUnload?: (window: BrowserWindow) => void;
|
||||
};
|
||||
export type PreloadPlugin<Config extends PluginBaseConfig> = BasePlugin<Config>;
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
type IF<T> = (args: T) => T;
|
||||
type Promisable<T> = T | Promise<T>;
|
||||
|
||||
export type PluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = {
|
||||
getConfig: () => Promisable<Config>;
|
||||
setConfig: (config: DeepPartial<Config>) => Promisable<void>;
|
||||
};
|
||||
|
||||
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;
|
||||
on: <Arguments extends unknown[]>(event: string, listener: (...args: Arguments) => Promisable<void>) => 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 MenuPluginContext<Config extends PluginBaseConfig = PluginBaseConfig> = PluginContext<Config> & {
|
||||
window: BrowserWindow;
|
||||
|
||||
refresh: () => 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: MenuPluginContext<Config>) => Promisable<MenuItemConstructorOptions[]>;
|
||||
|
||||
export type PluginBuilder<ID extends string, Config extends PluginBaseConfig> = {
|
||||
createRenderer: IF<RendererPluginFactory<Config>>;
|
||||
createMain: IF<MainPluginFactory<Config>>;
|
||||
createPreload: IF<PreloadPluginFactory<Config>>;
|
||||
createMenu: IF<MenuPluginFactory<Config>>;
|
||||
|
||||
id: ID;
|
||||
config: Config;
|
||||
name?: string;
|
||||
styles?: string[];
|
||||
restartNeeded: boolean;
|
||||
};
|
||||
export type PluginBuilderOptions<Config extends PluginBaseConfig = PluginBaseConfig> = {
|
||||
name?: string;
|
||||
restartNeeded: boolean;
|
||||
|
||||
config: Config;
|
||||
styles?: string[];
|
||||
}
|
||||
export const createPluginBuilder = <ID extends string, Config extends PluginBaseConfig>(
|
||||
id: ID,
|
||||
options: PluginBuilderOptions<Config>,
|
||||
): PluginBuilder<ID, Omit<Config, 'enabled'> & PluginBaseConfig> => ({
|
||||
createRenderer: (plugin) => plugin,
|
||||
createMain: (plugin) => plugin,
|
||||
createPreload: (plugin) => plugin,
|
||||
createMenu: (plugin) => plugin,
|
||||
|
||||
id,
|
||||
name: options.name,
|
||||
config: options.config,
|
||||
styles: options.styles,
|
||||
restartNeeded: options.restartNeeded,
|
||||
});
|
||||
@ -1 +0,0 @@
|
||||
export type PluginConfig<T extends keyof PluginBuilderList> = PluginBuilderList[T]['config'];
|
||||
@ -1,12 +1,12 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { PluginConfig } from '@/types/plugins';
|
||||
|
||||
export interface BaseContext {
|
||||
getConfig(): PluginConfig;
|
||||
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
|
||||
export interface BaseContext<Config extends PluginConfig> {
|
||||
getConfig(): Promise<Config>;
|
||||
setConfig(conf: Partial<Omit<Config, 'enabled'>>): void;
|
||||
}
|
||||
|
||||
export interface BackendContext extends BaseContext {
|
||||
export interface BackendContext<Config extends PluginConfig> extends BaseContext<Config> {
|
||||
ipc: {
|
||||
send: (event: string, ...args: unknown[]) => void;
|
||||
handle: (event: string, listener: CallableFunction) => void;
|
||||
@ -16,14 +16,14 @@ export interface BackendContext extends BaseContext {
|
||||
window: BrowserWindow;
|
||||
}
|
||||
|
||||
export interface MenuContext extends BaseContext {
|
||||
export interface MenuContext<Config extends PluginConfig> extends BaseContext<Config> {
|
||||
window: BrowserWindow;
|
||||
refresh: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface PreloadContext extends BaseContext {}
|
||||
export interface PreloadContext<Config extends PluginConfig> extends BaseContext<Config> {}
|
||||
|
||||
export interface RendererContext extends BaseContext {
|
||||
export interface RendererContext<Config extends PluginConfig> extends BaseContext<Config> {
|
||||
ipc: {
|
||||
send: (event: string, ...args: unknown[]) => void;
|
||||
invoke: (event: string, ...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
@ -11,31 +11,40 @@ type Author = string;
|
||||
|
||||
export type PluginConfig = {
|
||||
enabled: boolean;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
type PluginExtra = Record<string, unknown>;
|
||||
export type PluginLifecycleSimple<Context, This> = (this: This, ctx: Context) => void | Promise<void>;
|
||||
export type PluginLifecycleExtra<Config, Context, This> = This & {
|
||||
start?: PluginLifecycleSimple<Context, This>;
|
||||
stop?: PluginLifecycleSimple<Context, This>;
|
||||
onConfigChange?: (this: This, newConfig: Config) => void | Promise<void>;
|
||||
onPlayerApiReady?: (this: This, playerApi: YoutubePlayer) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type PluginLifecycleSimple<T> = (ctx: T) => void | Promise<void>;
|
||||
export type PluginLifecycleExtra<T> = {
|
||||
start?: PluginLifecycleSimple<T>;
|
||||
stop?: PluginLifecycleSimple<T>;
|
||||
onConfigChange?: (newConfig: PluginConfig) => void | Promise<void>;
|
||||
onPlayerApiReady?: (playerApi: YoutubePlayer) => void | Promise<void>;
|
||||
} & PluginExtra;
|
||||
export type PluginLifecycle<Config, Context, This> = PluginLifecycleSimple<Context, This> | PluginLifecycleExtra<Config, Context, This>;
|
||||
|
||||
export type PluginLifecycle<T> = PluginLifecycleSimple<T> | PluginLifecycleExtra<T>;
|
||||
|
||||
export interface PluginDef {
|
||||
export interface PluginDef<
|
||||
BackendProperties,
|
||||
PreloadProperties,
|
||||
RendererProperties,
|
||||
Config extends PluginConfig = PluginConfig,
|
||||
> {
|
||||
name: string;
|
||||
authors?: Author[];
|
||||
description?: string;
|
||||
config: PluginConfig;
|
||||
config?: Config;
|
||||
|
||||
menu?: (ctx: MenuContext) => Electron.MenuItemConstructorOptions[];
|
||||
menu?: (ctx: MenuContext<Config>) => Promise<Electron.MenuItemConstructorOptions[]>;
|
||||
stylesheets?: string[];
|
||||
restartNeeded?: boolean;
|
||||
|
||||
backend?: PluginLifecycle<BackendContext>;
|
||||
preload?: PluginLifecycle<PreloadContext>;
|
||||
renderer?: PluginLifecycle<RendererContext>;
|
||||
backend?: {
|
||||
[Key in keyof BackendProperties]: BackendProperties[Key]
|
||||
} & PluginLifecycle<Config, BackendContext<Config>, BackendProperties>;
|
||||
preload?: {
|
||||
[Key in keyof PreloadProperties]: PreloadProperties[Key]
|
||||
} & PluginLifecycle<Config, PreloadContext<Config>, PreloadProperties>;
|
||||
renderer?: {
|
||||
[Key in keyof RendererProperties]: RendererProperties[Key]
|
||||
} & PluginLifecycle<Config, RendererContext<Config>, RendererProperties>;
|
||||
}
|
||||
|
||||
@ -6,33 +6,43 @@ import type {
|
||||
|
||||
import type {
|
||||
PluginDef,
|
||||
PluginConfig,
|
||||
PluginLifecycleExtra,
|
||||
PluginLifecycleSimple,
|
||||
PluginConfig, PluginLifecycleExtra, PluginLifecycleSimple,
|
||||
} from '@/types/plugins';
|
||||
|
||||
export const createPlugin = (
|
||||
def: Omit<PluginDef, 'config'> & {
|
||||
config?: Omit<PluginConfig, 'enabled'>;
|
||||
export const createPlugin = <
|
||||
BackendProperties,
|
||||
PreloadProperties,
|
||||
RendererProperties,
|
||||
Config extends PluginConfig,
|
||||
>(
|
||||
def: PluginDef<
|
||||
BackendProperties,
|
||||
PreloadProperties,
|
||||
RendererProperties,
|
||||
Config
|
||||
> & {
|
||||
config?: Omit<Config, 'enabled'> & {
|
||||
enabled: boolean;
|
||||
};
|
||||
},
|
||||
): PluginDef => def as PluginDef;
|
||||
) => def;
|
||||
|
||||
type Options =
|
||||
| { ctx: 'backend'; context: BackendContext }
|
||||
| { ctx: 'preload'; context: PreloadContext }
|
||||
| { ctx: 'renderer'; context: RendererContext };
|
||||
type Options<Config extends PluginConfig> =
|
||||
| { ctx: 'backend'; context: BackendContext<Config> }
|
||||
| { ctx: 'preload'; context: PreloadContext<Config> }
|
||||
| { ctx: 'renderer'; context: RendererContext<Config> };
|
||||
|
||||
export const startPlugin = (id: string, def: PluginDef, options: Options) => {
|
||||
export const startPlugin = <Config extends PluginConfig>(id: string, def: PluginDef<unknown, unknown, unknown, Config>, options: Options<Config>) => {
|
||||
const lifecycle =
|
||||
typeof def[options.ctx] === 'function'
|
||||
? def[options.ctx] as PluginLifecycleSimple<Options['context']>
|
||||
: (def[options.ctx] as PluginLifecycleExtra<Options['context']>)?.start;
|
||||
? def[options.ctx] as PluginLifecycleSimple<Config, unknown>
|
||||
: (def[options.ctx] as PluginLifecycleExtra<Config, typeof options.context, unknown>)?.start;
|
||||
|
||||
if (!lifecycle) return false;
|
||||
|
||||
try {
|
||||
const start = performance.now();
|
||||
lifecycle(options.context);
|
||||
lifecycle(options.context as Config & typeof options.context);
|
||||
|
||||
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
|
||||
|
||||
@ -43,16 +53,16 @@ export const startPlugin = (id: string, def: PluginDef, options: Options) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const stopPlugin = (id: string, def: PluginDef, options: Options) => {
|
||||
export const stopPlugin = <Config extends PluginConfig>(id: string, def: PluginDef<unknown, unknown, unknown, Config>, options: Options<Config>) => {
|
||||
if (!def[options.ctx]) return false;
|
||||
if (typeof def[options.ctx] === 'function') return false;
|
||||
|
||||
const stop = def[options.ctx] as PluginLifecycleExtra<Options['context']>['stop'];
|
||||
const stop = def[options.ctx] as PluginLifecycleSimple<Config, unknown>;
|
||||
if (!stop) return false;
|
||||
|
||||
try {
|
||||
const start = performance.now();
|
||||
stop(options.context);
|
||||
stop(options.context as Config & typeof options.context);
|
||||
|
||||
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
|
||||
|
||||
|
||||
14
src/virtual-module.d.ts
vendored
14
src/virtual-module.d.ts
vendored
@ -1,13 +1,15 @@
|
||||
declare module 'virtual:plugins' {
|
||||
import type { PluginDef } from '@/types/plugins';
|
||||
import type { PluginConfig, PluginDef } from '@/types/plugins';
|
||||
|
||||
export const mainPlugins: Record<string, PluginDef>;
|
||||
export const menuPlugins: Record<string, PluginDef>;
|
||||
export const preloadPlugins: Record<string, PluginDef>;
|
||||
export const rendererPlugins: Record<string, PluginDef>;
|
||||
type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>;
|
||||
|
||||
export const mainPlugins: Record<string, Plugin>;
|
||||
export const menuPlugins: Record<string, Plugin>;
|
||||
export const preloadPlugins: Record<string, Plugin>;
|
||||
export const rendererPlugins: Record<string, Plugin>;
|
||||
|
||||
export const allPlugins: Record<
|
||||
string,
|
||||
Omit<PluginDef, 'backend' | 'preload' | 'renderer'>
|
||||
Omit<Plugin, 'backend' | 'preload' | 'renderer'>
|
||||
>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user