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