Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2023-11-27 00:18:34 +09:00
parent 3a1b77ebd8
commit 563d431c00
17 changed files with 563 additions and 679 deletions

View File

@ -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();
}
}
},
}
}
});

View File

@ -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,
);
}
}
}
};
});

View File

@ -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 });
},
})),
},
];
});

View File

@ -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();
}
}
}
}));

View File

@ -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));
}
}
});
},
}
}
});

View File

@ -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));
}
}
});
}
};
});

View File

@ -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?.();
}
}
}
});

View File

@ -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 });
},
},
];
});

View File

@ -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?.();
}
};
});

View File

@ -3,5 +3,6 @@ import style from './style.css?inline';
export default createPlugin({
name: 'Blur Navigation Bar',
renderer: { stylesheets: [style] },
stylesheets: [style],
renderer() {},
});

View File

@ -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,
});

View File

@ -1 +0,0 @@
export type PluginConfig<T extends keyof PluginBuilderList> = PluginBuilderList[T]['config'];