mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
feat(plugin): add onPlayerApiReady hook
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -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);
|
||||
|
||||
@ -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<HTMLElement>('#player-page');
|
||||
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||
ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||
playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||
sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||
sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||
ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
@ -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<HTMLElement>('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<HTMLElement>('tp-yt-paper-listbox');
|
||||
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
|
||||
} else {
|
||||
if (playerPage) {
|
||||
playerPage.style.backgroundColor = '#000000';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
||||
|
||||
$('video').addEventListener('srcChanged', videoChangeListener);
|
||||
captionsSettingsButton.addEventListener('click', captionsButtonClickListener);
|
||||
};
|
||||
|
||||
const removeListener = () => {
|
||||
$('.right-controls-buttons').removeChild(captionsSettingsButton);
|
||||
$<YoutubePlayer & HTMLElement>('#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<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
||||
|
||||
$('video').addEventListener('srcChanged', videoChangeListener);
|
||||
captionsSettingsButton.addEventListener('click', captionsButtonClickListener);
|
||||
},
|
||||
onUnload() {
|
||||
removeListener();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -5,7 +5,7 @@ import type { YoutubePlayer } from '../../types/youtube-player';
|
||||
export default builder.createRenderer(({ getConfig }) => {
|
||||
let config: Awaited<ReturnType<typeof getConfig>>;
|
||||
|
||||
let apiEvent: CustomEvent<YoutubePlayer>;
|
||||
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<HTMLVideoElement>('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;
|
||||
|
||||
@ -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<HTMLAnchorElement>('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<HTMLAnchorElement>('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,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -41,10 +41,7 @@ const exponentialVolume = () => {
|
||||
};
|
||||
|
||||
export default builder.createRenderer(() => ({
|
||||
onLoad() {
|
||||
return document.addEventListener('apiLoaded', exponentialVolume, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
onPlayerApiReady() {
|
||||
exponentialVolume();
|
||||
},
|
||||
}));
|
||||
|
||||
@ -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<Menu | null>('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 {');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
&& !$<HTMLElement & { opened: boolean }>('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)
|
||||
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
|
||||
) {
|
||||
togglePictureInPicture();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -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 = $<HTMLVideoElement>('video');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -32,19 +32,19 @@ export default builder.createRenderer(({ invoke }) => {
|
||||
api.setPlaybackQualityRange(newQuality);
|
||||
api.setPlaybackQuality(newQuality);
|
||||
});
|
||||
}
|
||||
|
||||
function setup(event: CustomEvent<YoutubePlayer>) {
|
||||
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);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -29,15 +29,14 @@ export default builder.createRenderer(({ on }) => {
|
||||
on('sponsorblock-skip', (_, segments: Segment[]) => {
|
||||
currentSegments = segments;
|
||||
});
|
||||
},
|
||||
onPlayerApiReady() {
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
const video = document.querySelector<HTMLVideoElement>('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<HTMLVideoElement>('video');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<Config extends PluginBaseConfig> = {
|
||||
onUnload?: () => void;
|
||||
onConfigChange?: (newConfig: Config) => void;
|
||||
}
|
||||
export type RendererPlugin<Config extends PluginBaseConfig> = BasePlugin<Config>;
|
||||
export type RendererPlugin<Config extends PluginBaseConfig> = BasePlugin<Config> & {
|
||||
onPlayerApiReady?: (api: YoutubePlayer) => void;
|
||||
};
|
||||
export type MainPlugin<Config extends PluginBaseConfig> = Omit<BasePlugin<Config>, 'onLoad' | 'onUnload'> & {
|
||||
onLoad?: (window: BrowserWindow) => void;
|
||||
onUnload?: (window: BrowserWindow) => void;
|
||||
|
||||
@ -22,8 +22,8 @@ export default builder.createRenderer(({ getConfig }) => {
|
||||
|
||||
const switchButtonDiv = ElementFromHtml(buttonTemplate);
|
||||
|
||||
function setup(e: CustomEvent<YoutubePlayer>) {
|
||||
api = e.detail;
|
||||
function setup(playerApi: YoutubePlayer) {
|
||||
api = playerApi;
|
||||
player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player');
|
||||
video = document.querySelector<HTMLVideoElement>('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;
|
||||
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
@ -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<string>();
|
||||
// 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 = $<HTMLVideoElement>('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<string>();
|
||||
// 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 = $<HTMLVideoElement>('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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
1
src/reset.d.ts
vendored
1
src/reset.d.ts
vendored
@ -13,7 +13,6 @@ declare global {
|
||||
}
|
||||
|
||||
interface DocumentEventMap {
|
||||
'apiLoaded': CustomEvent<YoutubePlayer>;
|
||||
'audioCanPlay': CustomEvent<Compressor>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user