feat(plugin): add onPlayerApiReady hook

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Su-Yong
2023-11-12 01:51:26 +09:00
parent 2097f42efb
commit a4f4ecb569
22 changed files with 273 additions and 291 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,10 +41,7 @@ const exponentialVolume = () => {
};
export default builder.createRenderer(() => ({
onLoad() {
return document.addEventListener('apiLoaded', exponentialVolume, {
once: true,
passive: true,
});
onPlayerApiReady() {
exponentialVolume();
},
}));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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