From 563d431c0074fca8f5aa4e16545000a514f04b5b Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Mon, 27 Nov 2023 00:18:34 +0900 Subject: [PATCH] WIP Co-authored-by: Su-Yong --- src/loader/renderer.ts | 6 +- src/plugins/adblocker/index.ts | 85 +++++- src/plugins/adblocker/main.ts | 43 ---- src/plugins/adblocker/menu.ts | 22 -- src/plugins/adblocker/preload.ts | 27 -- src/plugins/album-color-theme/index.ts | 142 ++++++++++- src/plugins/album-color-theme/renderer.ts | 137 ---------- src/plugins/ambient-mode/index.ts | 298 ++++++++++++++++++++-- src/plugins/ambient-mode/menu.ts | 89 ------- src/plugins/ambient-mode/renderer.ts | 184 ------------- src/plugins/blur-nav-bar/index.ts | 3 +- src/plugins/utils/builder.ts | 88 ------- src/plugins/utils/types.ts | 1 - src/types/contexts.ts | 14 +- src/types/plugins.ts | 43 ++-- src/utils/index.ts | 46 ++-- src/virtual-module.d.ts | 14 +- 17 files changed, 563 insertions(+), 679 deletions(-) delete mode 100644 src/plugins/adblocker/main.ts delete mode 100644 src/plugins/adblocker/menu.ts delete mode 100644 src/plugins/adblocker/preload.ts delete mode 100644 src/plugins/album-color-theme/renderer.ts delete mode 100644 src/plugins/ambient-mode/menu.ts delete mode 100644 src/plugins/ambient-mode/renderer.ts delete mode 100644 src/plugins/utils/builder.ts delete mode 100644 src/plugins/utils/types.ts diff --git a/src/loader/renderer.ts b/src/loader/renderer.ts index c17d5af9..8b7c4806 100644 --- a/src/loader/renderer.ts +++ b/src/loader/renderer.ts @@ -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 void)[]> = {}; -const loadedPluginMap: Record = {}; +const loadedPluginMap: Record> = {}; -const createContext = (id: string): RendererContext => ({ +const createContext = (id: string): RendererContext => ({ getConfig: () => window.mainConfig.plugins.getOptions(id), setConfig: async (newConfig) => { await window.ipcRenderer.invoke('set-config', id, newConfig); diff --git a/src/plugins/adblocker/index.ts b/src/plugins/adblocker/index.ts index 0f803ede..d144ed66 100644 --- a/src/plugins/adblocker/index.ts +++ b/src/plugins/adblocker/index.ts @@ -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(); + } + } + }, } -} +}); diff --git a/src/plugins/adblocker/main.ts b/src/plugins/adblocker/main.ts deleted file mode 100644 index 90c97827..00000000 --- a/src/plugins/adblocker/main.ts +++ /dev/null @@ -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, - ); - } - } - } - }; -}); diff --git a/src/plugins/adblocker/menu.ts b/src/plugins/adblocker/menu.ts deleted file mode 100644 index a88ec7e1..00000000 --- a/src/plugins/adblocker/menu.ts +++ /dev/null @@ -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 => { - 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 }); - }, - })), - }, - ]; -}); diff --git a/src/plugins/adblocker/preload.ts b/src/plugins/adblocker/preload.ts deleted file mode 100644 index f22596ab..00000000 --- a/src/plugins/adblocker/preload.ts +++ /dev/null @@ -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(); - } - } - } -})); diff --git a/src/plugins/album-color-theme/index.ts b/src/plugins/album-color-theme/index.ts index 9fbb2a8a..58ba7d7e 100644 --- a/src/plugins/album-color-theme/index.ts +++ b/src/plugins/album-color-theme/index.ts @@ -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('#player-page'); + this.navBarBackground = document.querySelector('#nav-bar-background'); + this.ytmusicPlayerBar = document.querySelector('ytmusic-player-bar'); + this.playerBarBackground = document.querySelector('#player-bar-background'); + this.sidebarBig = document.querySelector('#guide-wrapper'); + this.sidebarSmall = document.querySelector('#mini-guide-background'); + this.ytmusicAppLayout = document.querySelector('#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('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)); + } + } + }); + }, } -} +}); diff --git a/src/plugins/album-color-theme/renderer.ts b/src/plugins/album-color-theme/renderer.ts deleted file mode 100644 index 0e70c26f..00000000 --- a/src/plugins/album-color-theme/renderer.ts +++ /dev/null @@ -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('#player-page'); - navBarBackground = document.querySelector('#nav-bar-background'); - ytmusicPlayerBar = document.querySelector('ytmusic-player-bar'); - playerBarBackground = document.querySelector('#player-bar-background'); - sidebarBig = document.querySelector('#guide-wrapper'); - sidebarSmall = document.querySelector('#mini-guide-background'); - ytmusicAppLayout = document.querySelector('#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('tp-yt-paper-listbox'); - changeElementColor(ytRightClickList, hue, saturation, lightness - 15); - } else { - if (playerPage) { - playerPage.style.backgroundColor = '#000000'; - } - } - }) - .catch((e) => console.error(e)); - } - } - }); - } - }; -}); diff --git a/src/plugins/ambient-mode/index.ts b/src/plugins/ambient-mode/index.ts index 7e7ad93e..14cb5a47 100644 --- a/src/plugins/ambient-mode/index.ts +++ b/src/plugins/ambient-mode/index.ts @@ -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('#song-video'); + const video = document.querySelector('#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('#player-page'); + const ytmusicAppLayout = document.querySelector('#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?.(); + } } -} +}); diff --git a/src/plugins/ambient-mode/menu.ts b/src/plugins/ambient-mode/menu.ts deleted file mode 100644 index 7b9923c3..00000000 --- a/src/plugins/ambient-mode/menu.ts +++ /dev/null @@ -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 }); - }, - }, - ]; -}); diff --git a/src/plugins/ambient-mode/renderer.ts b/src/plugins/ambient-mode/renderer.ts deleted file mode 100644 index 11e462b8..00000000 --- a/src/plugins/ambient-mode/renderer.ts +++ /dev/null @@ -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('#song-video'); - const video = document.querySelector('#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('#player-page'); - const ytmusicAppLayout = document.querySelector('#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?.(); - } - }; -}); diff --git a/src/plugins/blur-nav-bar/index.ts b/src/plugins/blur-nav-bar/index.ts index 39f65599..86013e1d 100644 --- a/src/plugins/blur-nav-bar/index.ts +++ b/src/plugins/blur-nav-bar/index.ts @@ -3,5 +3,6 @@ import style from './style.css?inline'; export default createPlugin({ name: 'Blur Navigation Bar', - renderer: { stylesheets: [style] }, + stylesheets: [style], + renderer() {}, }); diff --git a/src/plugins/utils/builder.ts b/src/plugins/utils/builder.ts deleted file mode 100644 index 0c7bd655..00000000 --- a/src/plugins/utils/builder.ts +++ /dev/null @@ -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 = { - onLoad?: () => void; - onUnload?: () => void; - onConfigChange?: (newConfig: Config) => void; -} -export type RendererPlugin = BasePlugin & { - onPlayerApiReady?: (api: YoutubePlayer) => void; -}; -export type MainPlugin = Omit, 'onLoad' | 'onUnload'> & { - onLoad?: (window: BrowserWindow) => void; - onUnload?: (window: BrowserWindow) => void; -}; -export type PreloadPlugin = BasePlugin; - -type DeepPartial = { - [P in keyof T]?: DeepPartial; -}; -type IF = (args: T) => T; -type Promisable = T | Promise; - -export type PluginContext = { - getConfig: () => Promisable; - setConfig: (config: DeepPartial) => Promisable; -}; - -export type MainPluginContext = PluginContext & { - send: (event: string, ...args: unknown[]) => void; - handle: (event: string, listener: (...args: Arguments) => Promisable) => void; - on: (event: string, listener: (...args: Arguments) => Promisable) => void; -}; -export type RendererPluginContext = PluginContext & { - invoke: (event: string, ...args: unknown[]) => Promise; - on: (event: string, listener: (...args: Arguments) => Promisable) => void; -}; -export type MenuPluginContext = PluginContext & { - window: BrowserWindow; - - refresh: () => void; -}; - -export type RendererPluginFactory = (context: RendererPluginContext) => Promisable>; -export type MainPluginFactory = (context: MainPluginContext) => Promisable>; -export type PreloadPluginFactory = (context: PluginContext) => Promisable>; -export type MenuPluginFactory = (context: MenuPluginContext) => Promisable; - -export type PluginBuilder = { - createRenderer: IF>; - createMain: IF>; - createPreload: IF>; - createMenu: IF>; - - id: ID; - config: Config; - name?: string; - styles?: string[]; - restartNeeded: boolean; -}; -export type PluginBuilderOptions = { - name?: string; - restartNeeded: boolean; - - config: Config; - styles?: string[]; -} -export const createPluginBuilder = ( - id: ID, - options: PluginBuilderOptions, -): PluginBuilder & 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, -}); diff --git a/src/plugins/utils/types.ts b/src/plugins/utils/types.ts deleted file mode 100644 index a51a95a9..00000000 --- a/src/plugins/utils/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type PluginConfig = PluginBuilderList[T]['config']; diff --git a/src/types/contexts.ts b/src/types/contexts.ts index 9b73ee03..eb6c0a0d 100644 --- a/src/types/contexts.ts +++ b/src/types/contexts.ts @@ -1,12 +1,12 @@ import type { BrowserWindow } from 'electron'; import type { PluginConfig } from '@/types/plugins'; -export interface BaseContext { - getConfig(): PluginConfig; - setConfig(conf: Omit): void; +export interface BaseContext { + getConfig(): Promise; + setConfig(conf: Partial>): void; } -export interface BackendContext extends BaseContext { +export interface BackendContext extends BaseContext { 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 extends BaseContext { window: BrowserWindow; refresh: () => Promise | void; } -export interface PreloadContext extends BaseContext {} +export interface PreloadContext extends BaseContext {} -export interface RendererContext extends BaseContext { +export interface RendererContext extends BaseContext { ipc: { send: (event: string, ...args: unknown[]) => void; invoke: (event: string, ...args: unknown[]) => Promise; diff --git a/src/types/plugins.ts b/src/types/plugins.ts index da771582..53c1dad6 100644 --- a/src/types/plugins.ts +++ b/src/types/plugins.ts @@ -11,31 +11,40 @@ type Author = string; export type PluginConfig = { enabled: boolean; -} & Record; +}; -type PluginExtra = Record; +export type PluginLifecycleSimple = (this: This, ctx: Context) => void | Promise; +export type PluginLifecycleExtra = This & { + start?: PluginLifecycleSimple; + stop?: PluginLifecycleSimple; + onConfigChange?: (this: This, newConfig: Config) => void | Promise; + onPlayerApiReady?: (this: This, playerApi: YoutubePlayer) => void | Promise; +}; -export type PluginLifecycleSimple = (ctx: T) => void | Promise; -export type PluginLifecycleExtra = { - start?: PluginLifecycleSimple; - stop?: PluginLifecycleSimple; - onConfigChange?: (newConfig: PluginConfig) => void | Promise; - onPlayerApiReady?: (playerApi: YoutubePlayer) => void | Promise; -} & PluginExtra; +export type PluginLifecycle = PluginLifecycleSimple | PluginLifecycleExtra; -export type PluginLifecycle = PluginLifecycleSimple | PluginLifecycleExtra; - -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) => Promise; stylesheets?: string[]; restartNeeded?: boolean; - backend?: PluginLifecycle; - preload?: PluginLifecycle; - renderer?: PluginLifecycle; + backend?: { + [Key in keyof BackendProperties]: BackendProperties[Key] + } & PluginLifecycle, BackendProperties>; + preload?: { + [Key in keyof PreloadProperties]: PreloadProperties[Key] + } & PluginLifecycle, PreloadProperties>; + renderer?: { + [Key in keyof RendererProperties]: RendererProperties[Key] + } & PluginLifecycle, RendererProperties>; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 5c728236..041f04af 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,33 +6,43 @@ import type { import type { PluginDef, - PluginConfig, - PluginLifecycleExtra, - PluginLifecycleSimple, + PluginConfig, PluginLifecycleExtra, PluginLifecycleSimple, } from '@/types/plugins'; -export const createPlugin = ( - def: Omit & { - config?: Omit; +export const createPlugin = < + BackendProperties, + PreloadProperties, + RendererProperties, + Config extends PluginConfig, +>( + def: PluginDef< + BackendProperties, + PreloadProperties, + RendererProperties, + Config + > & { + config?: Omit & { + enabled: boolean; + }; }, -): PluginDef => def as PluginDef; +) => def; -type Options = - | { ctx: 'backend'; context: BackendContext } - | { ctx: 'preload'; context: PreloadContext } - | { ctx: 'renderer'; context: RendererContext }; +type Options = + | { ctx: 'backend'; context: BackendContext } + | { ctx: 'preload'; context: PreloadContext } + | { ctx: 'renderer'; context: RendererContext }; -export const startPlugin = (id: string, def: PluginDef, options: Options) => { +export const startPlugin = (id: string, def: PluginDef, options: Options) => { const lifecycle = typeof def[options.ctx] === 'function' - ? def[options.ctx] as PluginLifecycleSimple - : (def[options.ctx] as PluginLifecycleExtra)?.start; + ? def[options.ctx] as PluginLifecycleSimple + : (def[options.ctx] as PluginLifecycleExtra)?.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 = (id: string, def: PluginDef, options: Options) => { if (!def[options.ctx]) return false; if (typeof def[options.ctx] === 'function') return false; - const stop = def[options.ctx] as PluginLifecycleExtra['stop']; + const stop = def[options.ctx] as PluginLifecycleSimple; 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`); diff --git a/src/virtual-module.d.ts b/src/virtual-module.d.ts index 94d132ef..3c7d657c 100644 --- a/src/virtual-module.d.ts +++ b/src/virtual-module.d.ts @@ -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; - export const menuPlugins: Record; - export const preloadPlugins: Record; - export const rendererPlugins: Record; + type Plugin = PluginDef; + + export const mainPlugins: Record; + export const menuPlugins: Record; + export const preloadPlugins: Record; + export const rendererPlugins: Record; export const allPlugins: Record< string, - Omit + Omit >; }