diff --git a/src/index.ts b/src/index.ts index 440bb63e..8a03c2f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,7 +112,7 @@ const initHook = (win: BrowserWindow) => { const oldConfig = oldPluginConfigList[id] as PluginBaseConfig; const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig) as PluginBaseConfig; - if (config.enabled !== oldConfig.enabled) { + if (config.enabled !== oldConfig?.enabled) { if (config.enabled) { win.webContents.send('plugin:enable', id); ipcMain.emit('plugin:enable', id); diff --git a/src/plugins/album-color-theme/renderer.ts b/src/plugins/album-color-theme/renderer.ts index 2d79bf85..0e70c26f 100644 --- a/src/plugins/album-color-theme/renderer.ts +++ b/src/plugins/album-color-theme/renderer.ts @@ -63,15 +63,23 @@ export default builder.createRenderer(() => { } } + 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() { - 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'); + 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) { @@ -91,39 +99,38 @@ export default builder.createRenderer(() => { if (playerPage) { observer.observe(playerPage, { attributes: true }); } + }, + onPlayerApiReady(playerApi) { + const fastAverageColor = new FastAverageColor(); - 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'; - } + 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); } - }) - .catch((e) => console.error(e)); - } + 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/captions-selector/renderer.ts b/src/plugins/captions-selector/renderer.ts index 26aa9d59..633bfa53 100644 --- a/src/plugins/captions-selector/renderer.ts +++ b/src/plugins/captions-selector/renderer.ts @@ -5,8 +5,6 @@ import builder from './index'; import { ElementFromHtml } from '../utils/renderer'; import { YoutubePlayer } from '../../types/youtube-player'; -import type { ConfigType } from '../../config/dynamic'; - interface LanguageOptions { displayName: string; id: string | null; @@ -82,30 +80,24 @@ export default builder.createRenderer(({ getConfig, setConfig }) => { } }; - const listener = ({ detail }: { - detail: YoutubePlayer; - }) => { - api = detail; - $('.right-controls-buttons').append(captionsSettingsButton); - - captionTrackList = api.getOption('captions', 'tracklist') ?? []; - - $('video').addEventListener('srcChanged', videoChangeListener); - captionsSettingsButton.addEventListener('click', captionsButtonClickListener); - }; - const removeListener = () => { $('.right-controls-buttons').removeChild(captionsSettingsButton); $('#movie_player').unloadModule('captions'); - - document.removeEventListener('apiLoaded', listener); }; return { async onLoad() { config = await getConfig(); + }, + onPlayerApiReady(playerApi) { + api = playerApi; - document.addEventListener('apiLoaded', listener, { once: true, passive: true }); + $('.right-controls-buttons').append(captionsSettingsButton); + + captionTrackList = api.getOption('captions', 'tracklist') ?? []; + + $('video').addEventListener('srcChanged', videoChangeListener); + captionsSettingsButton.addEventListener('click', captionsButtonClickListener); }, onUnload() { removeListener(); diff --git a/src/plugins/crossfade/renderer.ts b/src/plugins/crossfade/renderer.ts index d84aa45b..520e365e 100644 --- a/src/plugins/crossfade/renderer.ts +++ b/src/plugins/crossfade/renderer.ts @@ -85,7 +85,7 @@ export default builder.createRenderer(({ getConfig, invoke }) => { }); // Exit just before the end for the transition - const transitionBeforeEnd = async () => { + const transitionBeforeEnd = () => { if ( video.currentTime >= video.duration - config.secondsBeforeEnd && isReadyToCrossfade() @@ -140,14 +140,11 @@ export default builder.createRenderer(({ getConfig, invoke }) => { }; return { - onLoad() { - document.addEventListener('apiLoaded', async () => { - config = await getConfig(); - onApiLoaded(); - }, { - once: true, - passive: true, - }); + async onLoad() { + config = await getConfig(); + }, + onPlayerApiReady() { + onApiLoaded(); }, onConfigChange(newConfig) { config = newConfig; diff --git a/src/plugins/disable-autoplay/renderer.ts b/src/plugins/disable-autoplay/renderer.ts index 71f974b2..c9557dc3 100644 --- a/src/plugins/disable-autoplay/renderer.ts +++ b/src/plugins/disable-autoplay/renderer.ts @@ -5,7 +5,7 @@ import type { YoutubePlayer } from '../../types/youtube-player'; export default builder.createRenderer(({ getConfig }) => { let config: Awaited>; - let apiEvent: CustomEvent; + let apiEvent: YoutubePlayer; const timeUpdateListener = (e: Event) => { if (e.target instanceof HTMLVideoElement) { @@ -15,27 +15,25 @@ export default builder.createRenderer(({ getConfig }) => { const eventListener = async (name: string) => { if (config.applyOnce) { - apiEvent.detail.removeEventListener('videodatachange', eventListener); + apiEvent.removeEventListener('videodatachange', eventListener); } if (name === 'dataloaded') { - apiEvent.detail.pauseVideo(); + apiEvent.pauseVideo(); document.querySelector('video')?.addEventListener('timeupdate', timeUpdateListener, { once: true }); } }; return { - async onLoad() { + async onPlayerApiReady(api) { config = await getConfig(); - document.addEventListener('apiLoaded', (api) => { - apiEvent = api; + apiEvent = api; - apiEvent.detail.addEventListener('videodatachange', eventListener); - }, { once: true, passive: true }); + apiEvent.addEventListener('videodatachange', eventListener); }, onUnload() { - apiEvent.detail.removeEventListener('videodatachange', eventListener); + apiEvent.removeEventListener('videodatachange', eventListener); }, onConfigChange(newConfig) { config = newConfig; diff --git a/src/plugins/downloader/renderer.ts b/src/plugins/downloader/renderer.ts index 94325eb6..6bab7b9a 100644 --- a/src/plugins/downloader/renderer.ts +++ b/src/plugins/downloader/renderer.ts @@ -14,35 +14,35 @@ const downloadButton = ElementFromHtml(downloadHTML); let doneFirstLoad = false; export default builder.createRenderer(({ invoke, on }) => { + const menuObserver = new MutationObserver(() => { + if (!menu) { + menu = getSongMenu(); + if (!menu) { + return; + } + } + + if (menu.contains(downloadButton)) { + return; + } + + const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href; + if (!menuUrl?.includes('watch?') && doneFirstLoad) { + return; + } + + menu.prepend(downloadButton); + progress = document.querySelector('#ytmcustom-download'); + + if (doneFirstLoad) { + return; + } + + setTimeout(() => doneFirstLoad ||= true, 500); + }); + return { onLoad() { - const menuObserver = new MutationObserver(() => { - if (!menu) { - menu = getSongMenu(); - if (!menu) { - return; - } - } - - if (menu.contains(downloadButton)) { - return; - } - - const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href; - if (!menuUrl?.includes('watch?') && doneFirstLoad) { - return; - } - - menu.prepend(downloadButton); - progress = document.querySelector('#ytmcustom-download'); - - if (doneFirstLoad) { - return; - } - - setTimeout(() => doneFirstLoad ||= true, 500); - }); - window.download = () => { let videoUrl = getSongMenu() // Selector of first button which is always "Start Radio" @@ -64,13 +64,6 @@ export default builder.createRenderer(({ invoke, on }) => { invoke('download-song', videoUrl); }; - document.addEventListener('apiLoaded', () => { - menuObserver.observe(document.querySelector('ytmusic-popup-container')!, { - childList: true, - subtree: true, - }); - }, { once: true, passive: true }); - on('downloader-feedback', (feedback: string) => { if (progress) { progress.innerHTML = feedback || 'Download'; @@ -78,6 +71,12 @@ export default builder.createRenderer(({ invoke, on }) => { console.warn('Cannot update progress'); } }); - } + }, + onPlayerApiReady() { + menuObserver.observe(document.querySelector('ytmusic-popup-container')!, { + childList: true, + subtree: true, + }); + }, }; }); diff --git a/src/plugins/exponential-volume/renderer.ts b/src/plugins/exponential-volume/renderer.ts index 05abbfc4..ff2b3153 100644 --- a/src/plugins/exponential-volume/renderer.ts +++ b/src/plugins/exponential-volume/renderer.ts @@ -41,10 +41,7 @@ const exponentialVolume = () => { }; export default builder.createRenderer(() => ({ - onLoad() { - return document.addEventListener('apiLoaded', exponentialVolume, { - once: true, - passive: true, - }); + onPlayerApiReady() { + exponentialVolume(); }, })); diff --git a/src/plugins/in-app-menu/renderer.ts b/src/plugins/in-app-menu/renderer.ts index 998fa971..a30f2144 100644 --- a/src/plugins/in-app-menu/renderer.ts +++ b/src/plugins/in-app-menu/renderer.ts @@ -17,7 +17,7 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { return { async onLoad() { const config = await getConfig(); - + const hideDOMWindowControls = config.hideDOMWindowControls; let hideMenu = window.mainConfig.get('options.hideMenu'); @@ -26,13 +26,13 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { let maximizeButton: HTMLButtonElement; let panelClosers: (() => void)[] = []; if (isMacOS) titleBar.style.setProperty('--offset-left', '70px'); - + const logo = document.createElement('img'); const close = document.createElement('img'); const minimize = document.createElement('img'); const maximize = document.createElement('img'); const unmaximize = document.createElement('img'); - + if (window.ELECTRON_RENDERER_URL) { logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw; close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw; @@ -46,7 +46,7 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { maximize.src = maximizeRaw; unmaximize.src = unmaximizeRaw; } - + logo.classList.add('title-bar-icon'); const logoClick = () => { hideMenu = !hideMenu; @@ -62,22 +62,22 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { }); }; logo.onclick = logoClick; - + on('toggle-in-app-menu', logoClick); - + if (!isMacOS) titleBar.appendChild(logo); document.body.appendChild(titleBar); - + titleBar.appendChild(logo); - + const addWindowControls = async () => { - + // Create window control buttons const minimizeButton = document.createElement('button'); minimizeButton.classList.add('window-control'); minimizeButton.appendChild(minimize); minimizeButton.onclick = () => invoke('window-minimize'); - + maximizeButton = document.createElement('button'); if (await invoke('window-is-maximized')) { maximizeButton.classList.add('window-control'); @@ -91,37 +91,37 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { // change icon to maximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(maximize); - + // call unmaximize await invoke('window-unmaximize'); } else { // change icon to unmaximize maximizeButton.removeChild(maximizeButton.firstChild!); maximizeButton.appendChild(unmaximize); - + // call maximize await invoke('window-maximize'); } }; - + const closeButton = document.createElement('button'); closeButton.classList.add('window-control'); closeButton.appendChild(close); closeButton.onclick = () => invoke('window-close'); - + // Create a container div for the window control buttons const windowControlsContainer = document.createElement('div'); windowControlsContainer.classList.add('window-controls-container'); windowControlsContainer.appendChild(minimizeButton); windowControlsContainer.appendChild(maximizeButton); windowControlsContainer.appendChild(closeButton); - + // Add window control buttons to the title bar titleBar.appendChild(windowControlsContainer); }; - + if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); - + if (navBar) { const observer = new MutationObserver((mutations) => { mutations.forEach(() => { @@ -129,25 +129,25 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); }); }); - + observer.observe(navBar, { attributes : true, attributeFilter : ['style'] }); } - + const updateMenu = async () => { const children = [...titleBar.children]; children.forEach((child) => { if (child !== logo) child.remove(); }); panelClosers = []; - + const menu = await invoke('get-menu'); if (!menu) return; - + menu.items.forEach((menuItem) => { const menu = document.createElement('menu-button'); const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []); panelClosers.push(closer); - + menu.append(menuItem.label); titleBar.appendChild(menu); if (hideMenu) { @@ -159,7 +159,7 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { await updateMenu(); document.title = 'Youtube Music'; - + on('close-all-in-app-menu-panel', () => { panelClosers.forEach((closer) => closer()); }); @@ -176,21 +176,20 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => { maximizeButton.appendChild(unmaximize); } }); - + if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { on('pip-toggle', () => { updateMenu(); }); } - - // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) - document.addEventListener('apiLoaded', () => { - const htmlHeadStyle = document.querySelector('head > div > style'); - if (htmlHeadStyle) { - // HACK: This is a hack to remove the scrollbar width - htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); - } - }, { once: true, passive: true }); - } + }, + // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) + onPlayerApiReady() { + const htmlHeadStyle = document.querySelector('head > div > style'); + if (htmlHeadStyle) { + // HACK: This is a hack to remove the scrollbar width + htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); + } + }, }; }); diff --git a/src/plugins/notifications/interactive.ts b/src/plugins/notifications/interactive.ts index 90c09067..a672fceb 100644 --- a/src/plugins/notifications/interactive.ts +++ b/src/plugins/notifications/interactive.ts @@ -211,7 +211,7 @@ export default ( songControls = getSongControls(win); let currentSeconds = 0; - on('apiLoaded', () => send('setupTimeChangedListener')); + on('ytmd:player-api-loaded', () => send('setupTimeChangedListener')); on('timeChanged', (t: number) => { currentSeconds = t; diff --git a/src/plugins/picture-in-picture/renderer.ts b/src/plugins/picture-in-picture/renderer.ts index 8a697237..2f6b7a02 100644 --- a/src/plugins/picture-in-picture/renderer.ts +++ b/src/plugins/picture-in-picture/renderer.ts @@ -133,11 +133,27 @@ const listenForToggle = () => { }); }; -function observeMenu(options: PictureInPicturePluginConfig) { - useNativePiP = options.useNativePiP; - document.addEventListener( - 'apiLoaded', - () => { + +export default builder.createRenderer(({ getConfig }) => { + return { + async onLoad() { + const config = await getConfig(); + + useNativePiP = config.useNativePiP; + + if (config.hotkey) { + const hotkeyEvent = toKeyEvent(config.hotkey); + window.addEventListener('keydown', (event) => { + if ( + keyEventAreEqual(event, hotkeyEvent) + && !$('ytmusic-search-box')?.opened + ) { + togglePictureInPicture(); + } + }); + } + }, + onPlayerApiReady() { listenForToggle(); cloneButton('.player-minimize-button')?.addEventListener('click', async () => { @@ -154,28 +170,5 @@ function observeMenu(options: PictureInPicturePluginConfig) { subtree: true, }); }, - { once: true, passive: true }, - ); -} - -export default builder.createRenderer(({ getConfig }) => { - return { - async onLoad() { - const config = await getConfig(); - - observeMenu(config); - - if (config.hotkey) { - const hotkeyEvent = toKeyEvent(config.hotkey); - window.addEventListener('keydown', (event) => { - if ( - keyEventAreEqual(event, hotkeyEvent) - && !$('ytmusic-search-box')?.opened - ) { - togglePictureInPicture(); - } - }); - } - } }; }); diff --git a/src/plugins/playback-speed/renderer.ts b/src/plugins/playback-speed/renderer.ts index e2cef0fc..6725432b 100644 --- a/src/plugins/playback-speed/renderer.ts +++ b/src/plugins/playback-speed/renderer.ts @@ -116,12 +116,10 @@ function forcePlaybackRate(e: Event) { export default builder.createRenderer(() => { return { - onLoad() { - document.addEventListener('apiLoaded', () => { - observePopupContainer(); - observeVideo(); - setupWheelListener(); - }, { once: true, passive: true }); + onPlayerApiReady() { + observePopupContainer(); + observeVideo(); + setupWheelListener(); }, onUnload() { const video = $('video'); diff --git a/src/plugins/precise-volume/renderer.ts b/src/plugins/precise-volume/renderer.ts index c878c1fe..bd9d2905 100644 --- a/src/plugins/precise-volume/renderer.ts +++ b/src/plugins/precise-volume/renderer.ts @@ -258,13 +258,13 @@ export default builder.createRenderer(async ({ on, getConfig, setConfig }) => { return { onLoad() { overrideListener(); + }, + onPlayerApiReady(playerApi) { + api = playerApi; - document.addEventListener('apiLoaded', (e) => { - api = e.detail; - on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease)); - on('setVolume', (value: number) => setVolume(value)); - firstRun(); - }, { once: true, passive: true }); + on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease)); + on('setVolume', (value: number) => setVolume(value)); + firstRun(); }, onConfigChange(config) { options = config; diff --git a/src/plugins/quality-changer/renderer.ts b/src/plugins/quality-changer/renderer.ts index 1da2d7e7..896ed88e 100644 --- a/src/plugins/quality-changer/renderer.ts +++ b/src/plugins/quality-changer/renderer.ts @@ -32,19 +32,19 @@ export default builder.createRenderer(({ invoke }) => { api.setPlaybackQualityRange(newQuality); api.setPlaybackQuality(newQuality); }); - } - - function setup(event: CustomEvent) { - api = event.detail; + }; + function setup() { $('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton); qualitySettingsButton.addEventListener('click', chooseQuality); } return { - onLoad() { - document.addEventListener('apiLoaded', setup, { once: true, passive: true }); + onPlayerApiReady(playerApi) { + api = playerApi; + + setup(); }, onUnload() { $('.top-row-buttons.ytmusic-player')?.removeChild(qualitySettingsButton); diff --git a/src/plugins/shortcuts/mpris.ts b/src/plugins/shortcuts/mpris.ts index 4365ae56..4b68d720 100644 --- a/src/plugins/shortcuts/mpris.ts +++ b/src/plugins/shortcuts/mpris.ts @@ -32,7 +32,7 @@ function registerMPRIS(win: BrowserWindow) { const player = setupMPRIS(); - ipcMain.handle('apiLoaded', () => { + ipcMain.on('ytmd:player-api-loaded', () => { win.webContents.send('setupSeekedListener', 'mpris'); win.webContents.send('setupTimeChangedListener', 'mpris'); win.webContents.send('setupRepeatChangedListener', 'mpris'); diff --git a/src/plugins/sponsorblock/renderer.ts b/src/plugins/sponsorblock/renderer.ts index e1772183..5378597b 100644 --- a/src/plugins/sponsorblock/renderer.ts +++ b/src/plugins/sponsorblock/renderer.ts @@ -29,15 +29,14 @@ export default builder.createRenderer(({ on }) => { on('sponsorblock-skip', (_, segments: Segment[]) => { currentSegments = segments; }); + }, + onPlayerApiReady() { + const video = document.querySelector('video'); + if (!video) return; - document.addEventListener('apiLoaded', () => { - const video = document.querySelector('video'); - if (!video) return; - - video.addEventListener('timeupdate', timeUpdateListener); - // Reset segments on song end - video.addEventListener('emptied', resetSegments); - }, { once: true, passive: true }); + video.addEventListener('timeupdate', timeUpdateListener); + // Reset segments on song end + video.addEventListener('emptied', resetSegments); }, onUnload() { const video = document.querySelector('video'); diff --git a/src/plugins/tuna-obs/main.ts b/src/plugins/tuna-obs/main.ts index 4c1cc4aa..542cde9a 100644 --- a/src/plugins/tuna-obs/main.ts +++ b/src/plugins/tuna-obs/main.ts @@ -54,7 +54,7 @@ const post = (data: Data) => { export default builder.createMain(({ send, handle, on }) => { return { onLoad() { - on('apiLoaded', () => send('setupTimeChangedListener')); + on('ytmd:player-api-loaded', () => send('setupTimeChangedListener')); on('timeChanged', (t: number) => { if (!data.title) { return; diff --git a/src/plugins/utils/builder.ts b/src/plugins/utils/builder.ts index 65340d44..965edf55 100644 --- a/src/plugins/utils/builder.ts +++ b/src/plugins/utils/builder.ts @@ -2,6 +2,7 @@ import type { BrowserWindow, MenuItemConstructorOptions, } from 'electron'; +import type { YoutubePlayer } from '../../types/youtube-player'; export type PluginBaseConfig = { enabled: boolean; @@ -11,7 +12,9 @@ export type BasePlugin = { onUnload?: () => void; onConfigChange?: (newConfig: Config) => void; } -export type RendererPlugin = BasePlugin; +export type RendererPlugin = BasePlugin & { + onPlayerApiReady?: (api: YoutubePlayer) => void; +}; export type MainPlugin = Omit, 'onLoad' | 'onUnload'> & { onLoad?: (window: BrowserWindow) => void; onUnload?: (window: BrowserWindow) => void; diff --git a/src/plugins/video-toggle/renderer.ts b/src/plugins/video-toggle/renderer.ts index 91f877f8..4cf43e7c 100644 --- a/src/plugins/video-toggle/renderer.ts +++ b/src/plugins/video-toggle/renderer.ts @@ -22,8 +22,8 @@ export default builder.createRenderer(({ getConfig }) => { const switchButtonDiv = ElementFromHtml(buttonTemplate); - function setup(e: CustomEvent) { - api = e.detail; + function setup(playerApi: YoutubePlayer) { + api = playerApi; player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player'); video = document.querySelector('video'); @@ -194,13 +194,11 @@ export default builder.createRenderer(({ getConfig }) => { document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher'); return; } - - default: - case 'custom': { - document.addEventListener('apiLoaded', setup, { once: true, passive: true }); - } } }, + onPlayerApiReady(playerApi) { + if (config.mode !== 'native' && config.mode != 'disabled') setup(playerApi); + }, onConfigChange(newConfig) { config = newConfig; diff --git a/src/providers/song-controls-front.ts b/src/providers/song-controls-front.ts deleted file mode 100644 index be080c81..00000000 --- a/src/providers/song-controls-front.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const setupSongControls = () => { - document.addEventListener('apiLoaded', (event) => { - window.ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t)); - window.ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t)); - }, { once: true, passive: true }); -}; diff --git a/src/providers/song-info-front.ts b/src/providers/song-info-front.ts index 05ae3f2a..57b91a91 100644 --- a/src/providers/song-info-front.ts +++ b/src/providers/song-info-front.ts @@ -73,74 +73,72 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => { window.ipcRenderer.send('volumeChanged', api.getVolume()); }); -export default () => { - document.addEventListener('apiLoaded', (apiEvent) => { - window.ipcRenderer.on('setupTimeChangedListener', () => { - setupTimeChangedListener(); - }); +export default (api: YoutubePlayer) => { + window.ipcRenderer.on('setupTimeChangedListener', () => { + setupTimeChangedListener(); + }); - window.ipcRenderer.on('setupRepeatChangedListener', () => { - setupRepeatChangedListener(); - }); + window.ipcRenderer.on('setupRepeatChangedListener', () => { + setupRepeatChangedListener(); + }); - window.ipcRenderer.on('setupVolumeChangedListener', () => { - setupVolumeChangedListener(apiEvent.detail); - }); + window.ipcRenderer.on('setupVolumeChangedListener', () => { + setupVolumeChangedListener(api); + }); - window.ipcRenderer.on('setupSeekedListener', () => { - setupSeekedListener(); - }); + window.ipcRenderer.on('setupSeekedListener', () => { + setupSeekedListener(); + }); - const playPausedHandler = (e: Event, status: string) => { - if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) { - window.ipcRenderer.send('playPaused', { - isPaused: status === 'pause', - elapsedSeconds: Math.floor(e.target.currentTime), - }); + const playPausedHandler = (e: Event, status: string) => { + if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) { + window.ipcRenderer.send('playPaused', { + isPaused: status === 'pause', + elapsedSeconds: Math.floor(e.target.currentTime), + }); + } + }; + + const playPausedHandlers = { + playing: (e: Event) => playPausedHandler(e, 'playing'), + pause: (e: Event) => playPausedHandler(e, 'pause'), + }; + + const waitingEvent = new Set(); + // Name = "dataloaded" and abit later "dataupdated" + api.addEventListener('videodatachange', (name: string, videoData) => { + if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) { + waitingEvent.delete(videoData.videoId); + sendSongInfo(videoData); + } else if (name === 'dataloaded') { + const video = $('video'); + video?.dispatchEvent(srcChangedEvent); + + for (const status of ['playing', 'pause'] as const) { // for fix issue that pause event not fired + video?.addEventListener(status, playPausedHandlers[status]); } - }; - const playPausedHandlers = { - playing: (e: Event) => playPausedHandler(e, 'playing'), - pause: (e: Event) => playPausedHandler(e, 'pause'), - }; + waitingEvent.add(videoData.videoId); + } + }); - const waitingEvent = new Set(); - // Name = "dataloaded" and abit later "dataupdated" - apiEvent.detail.addEventListener('videodatachange', (name: string, videoData) => { - if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) { - waitingEvent.delete(videoData.videoId); - sendSongInfo(videoData); - } else if (name === 'dataloaded') { - const video = $('video'); - video?.dispatchEvent(srcChangedEvent); + const video = $('video')!; + for (const status of ['playing', 'pause'] as const) { + video.addEventListener(status, playPausedHandlers[status]); + } - for (const status of ['playing', 'pause'] as const) { // for fix issue that pause event not fired - video?.addEventListener(status, playPausedHandlers[status]); - } + function sendSongInfo(videoData: VideoDataChangeValue) { + const data = api.getPlayerResponse(); - waitingEvent.add(videoData.videoId); - } - }); + data.videoDetails.album = videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(0)?.text; + data.videoDetails.elapsedSeconds = 0; + data.videoDetails.isPaused = false; - const video = $('video')!; - for (const status of ['playing', 'pause'] as const) { - video.addEventListener(status, playPausedHandlers[status]); + // HACK: This is a workaround for "podcast" type video. GREAT JOB GOOGLE. + if (data.playabilityStatus.transportControlsConfig) { + data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name; } - function sendSongInfo(videoData: VideoDataChangeValue) { - const data = apiEvent.detail.getPlayerResponse(); - - data.videoDetails.album = videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(0)?.text; - data.videoDetails.elapsedSeconds = 0; - data.videoDetails.isPaused = false; - - // HACK: This is a workaround for "podcast" type video. GREAT JOB GOOGLE. - if (data.playabilityStatus.transportControlsConfig) { - data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name; - } - - window.ipcRenderer.send('video-src-changed', data); - } - }, { once: true, passive: true }); + window.ipcRenderer.send('video-src-changed', data); + } }; diff --git a/src/renderer.ts b/src/renderer.ts index ec9f1743..bab99200 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -6,22 +6,23 @@ import { pluginBuilders } from 'virtual:PluginBuilders'; import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugins/utils/builder'; import { startingPages } from './providers/extracted-data'; -import { setupSongControls } from './providers/song-controls-front'; import setupSongInfo from './providers/song-info-front'; import { forceLoadRendererPlugin, forceUnloadRendererPlugin, - getAllLoadedRendererPlugins, + getAllLoadedRendererPlugins, getLoadedRendererPlugin, loadAllRendererPlugins, registerRendererPlugin } from './loader/renderer'; +import { YoutubePlayer } from './types/youtube-player'; -let api: Element | null = null; +let api: (Element & YoutubePlayer) | null = null; function listenForApiLoad() { api = document.querySelector('#movie_player'); if (api) { onApiLoaded(); + return; } @@ -29,6 +30,7 @@ function listenForApiLoad() { api = document.querySelector('#movie_player'); if (api) { observer.disconnect(); + onApiLoaded(); } }); @@ -41,6 +43,12 @@ interface YouTubeMusicAppElement extends HTMLElement { } function onApiLoaded() { + window.ipcRenderer.on('seekTo', (_, t: number) => api!.seekTo(t)); + window.ipcRenderer.on('seekBy', (_, t: number) => api!.seekBy(t)); + + // Inject song-info provider + setupSongInfo(api!); + const video = document.querySelector('video')!; const audioContext = new AudioContext(); const audioSource = audioContext.createMediaElementSource(video); @@ -66,10 +74,14 @@ function onApiLoaded() { ); }, { passive: true }, - );! + ); - document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); - window.ipcRenderer.send('apiLoaded'); + Object.values(getAllLoadedRendererPlugins()) + .forEach((plugin) => { + plugin.onPlayerApiReady?.(api!); + }); + + window.ipcRenderer.send('ytmd:player-api-loaded'); // Navigate to "Starting page" const startingPage: string = window.mainConfig.get('options.startingPage'); @@ -112,8 +124,13 @@ function onApiLoaded() { window.ipcRenderer.on('plugin:unload', (_event, id: keyof PluginBuilderList) => { forceUnloadRendererPlugin(id); }); - window.ipcRenderer.on('plugin:enable', (_event, id: keyof PluginBuilderList) => { - forceLoadRendererPlugin(id); + window.ipcRenderer.on('plugin:enable', async (_event, id: keyof PluginBuilderList) => { + await forceLoadRendererPlugin(id); + if (api) { + const plugin = getLoadedRendererPlugin(id); + + if (plugin) plugin.onPlayerApiReady?.(api); + } }); window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => { @@ -122,12 +139,6 @@ function onApiLoaded() { if (plugin) plugin.onConfigChange?.(newConfig); }); - // Inject song-info provider - setupSongInfo(); - - // Inject song-controls - setupSongControls(); - // Wait for complete load of YouTube api listenForApiLoad(); diff --git a/src/reset.d.ts b/src/reset.d.ts index a6af3678..6cf2792d 100644 --- a/src/reset.d.ts +++ b/src/reset.d.ts @@ -13,7 +13,6 @@ declare global { } interface DocumentEventMap { - 'apiLoaded': CustomEvent; 'audioCanPlay': CustomEvent; }