mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 12:21:47 +00:00
feat: migrate from raw HTML to JSX (TSX / SolidJS) (#3583)
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -3,7 +3,7 @@ import { createPlugin } from '@/utils';
|
||||
|
||||
import { onConfigChange, onMainLoad } from './main';
|
||||
import { onMenu } from './menu';
|
||||
import { onPlayerApiReady, onRendererLoad } from './renderer';
|
||||
import { onPlayerApiReady } from './renderer';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
export type PictureInPicturePluginConfig = {
|
||||
@ -41,7 +41,6 @@ export default createPlugin({
|
||||
onConfigChange,
|
||||
},
|
||||
renderer: {
|
||||
start: onRendererLoad,
|
||||
onPlayerApiReady,
|
||||
},
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ export const onMainLoad = async ({
|
||||
window.setMaximizable(false);
|
||||
window.setFullScreenable(false);
|
||||
|
||||
send('pip-toggle', true);
|
||||
send('ytmd:pip-toggle', true);
|
||||
|
||||
app.dock?.hide();
|
||||
window.setVisibleOnAllWorkspaces(true, {
|
||||
@ -63,7 +63,7 @@ export const onMainLoad = async ({
|
||||
window.setMaximizable(true);
|
||||
window.setFullScreenable(true);
|
||||
|
||||
send('pip-toggle', false);
|
||||
send('ytmd:pip-toggle', false);
|
||||
|
||||
window.setVisibleOnAllWorkspaces(false);
|
||||
window.setAlwaysOnTop(false);
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||
|
||||
import pipHTML from './templates/picture-in-picture.html?raw';
|
||||
|
||||
import { getSongMenu } from '@/providers/dom-elements';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import type { PictureInPicturePluginConfig } from './index';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
let useNativePiP = false;
|
||||
let menu: Element | null = null;
|
||||
const pipButton = ElementFromHtml(pipHTML);
|
||||
|
||||
let doneFirstLoad = false;
|
||||
|
||||
// Will also clone
|
||||
function replaceButton(query: string, button: Element) {
|
||||
const svg = button.querySelector('#icon svg')?.cloneNode(true);
|
||||
if (svg) {
|
||||
button.replaceWith(button.cloneNode(true));
|
||||
button.remove();
|
||||
const newButton = $(query);
|
||||
if (newButton) {
|
||||
newButton.querySelector('#icon')?.append(svg);
|
||||
}
|
||||
return newButton;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cloneButton(query: string) {
|
||||
const button = $(query);
|
||||
if (button) {
|
||||
replaceButton(query, button);
|
||||
}
|
||||
return $(query);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
menu.contains(pipButton) ||
|
||||
!(
|
||||
menu.parentElement as (HTMLElement & { eventSink_: Element }) | null
|
||||
)?.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for video (or music)
|
||||
let menuUrl = $<HTMLAnchorElement>(
|
||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
||||
)?.href;
|
||||
|
||||
if (!menuUrl?.includes('watch?')) {
|
||||
menuUrl = undefined;
|
||||
// check for podcast
|
||||
for (const it of document.querySelectorAll(
|
||||
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
||||
)) {
|
||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
||||
menuUrl = it.getAttribute('href')!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!menuUrl && doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.prepend(pipButton);
|
||||
|
||||
if (!doneFirstLoad) {
|
||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
||||
}
|
||||
});
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
if (useNativePiP) {
|
||||
const isInPiP = document.pictureInPictureElement !== null;
|
||||
const video = $<HTMLVideoElement>('video');
|
||||
const togglePiP = () =>
|
||||
isInPiP
|
||||
? document.exitPictureInPicture.call(document)
|
||||
: video?.requestPictureInPicture?.call(video);
|
||||
|
||||
try {
|
||||
await togglePiP();
|
||||
$<HTMLButtonElement>('#icon')?.click(); // Close the menu
|
||||
return true;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
window.ipcRenderer.send('plugin:toggle-picture-in-picture');
|
||||
return false;
|
||||
};
|
||||
// For UI (HTML)
|
||||
window.togglePictureInPicture = togglePictureInPicture;
|
||||
|
||||
const listenForToggle = () => {
|
||||
const originalExitButton = $<HTMLButtonElement>('.exit-fullscreen-button');
|
||||
const appLayout = $<HTMLElement>('ytmusic-app-layout');
|
||||
const expandMenu = $<HTMLElement>('#expanding-menu');
|
||||
const middleControls = $<HTMLButtonElement>('.middle-controls');
|
||||
const playerPage = $<HTMLElement & { playerPageOpen_: boolean }>(
|
||||
'ytmusic-player-page',
|
||||
);
|
||||
const togglePlayerPageButton = $<HTMLButtonElement>(
|
||||
'.toggle-player-page-button',
|
||||
);
|
||||
const fullScreenButton = $<HTMLButtonElement>('.fullscreen-button');
|
||||
const player = $<
|
||||
HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }
|
||||
>('#player');
|
||||
const onPlayerDblClick = player?.onDoubleClick_;
|
||||
const mouseLeaveEventListener = () => middleControls?.click();
|
||||
|
||||
const titlebar = $<HTMLElement>('.cet-titlebar');
|
||||
|
||||
window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => {
|
||||
if (originalExitButton && player) {
|
||||
if (isPip) {
|
||||
replaceButton(
|
||||
'.exit-fullscreen-button',
|
||||
originalExitButton,
|
||||
)?.addEventListener('click', () => togglePictureInPicture());
|
||||
player.onDoubleClick_ = () => {};
|
||||
|
||||
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
|
||||
if (!playerPage?.playerPageOpen_) {
|
||||
togglePlayerPageButton?.click();
|
||||
}
|
||||
|
||||
fullScreenButton?.click();
|
||||
appLayout?.classList.add('pip');
|
||||
if (titlebar) {
|
||||
titlebar.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
$('.exit-fullscreen-button')?.replaceWith(originalExitButton);
|
||||
player.onDoubleClick_ = onPlayerDblClick;
|
||||
expandMenu?.removeEventListener('mouseleave', mouseLeaveEventListener);
|
||||
originalExitButton.click();
|
||||
appLayout?.classList.remove('pip');
|
||||
if (titlebar) {
|
||||
titlebar.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const onRendererLoad = async ({
|
||||
getConfig,
|
||||
}: RendererContext<PictureInPicturePluginConfig>) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const onPlayerApiReady = () => {
|
||||
listenForToggle();
|
||||
|
||||
cloneButton('.player-minimize-button')?.addEventListener(
|
||||
'click',
|
||||
async () => {
|
||||
await togglePictureInPicture();
|
||||
setTimeout(() => $<HTMLButtonElement>('#player')?.click());
|
||||
},
|
||||
);
|
||||
|
||||
// Allows easily closing the menu by programmatically clicking outside of it
|
||||
$('#expanding-menu')?.removeAttribute('no-cancel-on-outside-click');
|
||||
// TODO: think about wether an additional button in songMenu is needed
|
||||
const popupContainer = $('ytmusic-popup-container');
|
||||
if (popupContainer)
|
||||
observer.observe(popupContainer, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
166
src/plugins/picture-in-picture/renderer.tsx
Normal file
166
src/plugins/picture-in-picture/renderer.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { getSongMenu } from '@/providers/dom-elements';
|
||||
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { PictureInPictureButton } from './templates/picture-in-picture-button';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { PictureInPicturePluginConfig } from './index';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
export const onPlayerApiReady = async (
|
||||
_: YoutubePlayer,
|
||||
{ ipc, getConfig }: RendererContext<PictureInPicturePluginConfig>,
|
||||
) => {
|
||||
const config = await getConfig();
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
if ((await getConfig()).useNativePiP) {
|
||||
const isInPiP = document.pictureInPictureElement !== null;
|
||||
const video = document.querySelector<HTMLVideoElement>('video');
|
||||
const togglePiP = () =>
|
||||
isInPiP
|
||||
? document.exitPictureInPicture()
|
||||
: video?.requestPictureInPicture();
|
||||
|
||||
try {
|
||||
await togglePiP();
|
||||
document.querySelector<HTMLButtonElement>('#icon')?.click(); // Close the menu
|
||||
return true;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
ipc.send('plugin:toggle-picture-in-picture');
|
||||
return false;
|
||||
};
|
||||
|
||||
if (config.hotkey) {
|
||||
const hotkeyEvent = toKeyEvent(config.hotkey);
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (
|
||||
keyEventAreEqual(event, hotkeyEvent) &&
|
||||
!document.querySelector<HTMLElement & { opened: boolean }>(
|
||||
'ytmusic-search-box',
|
||||
)?.opened
|
||||
) {
|
||||
togglePictureInPicture();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const exitFullScreenButton = document.querySelector<HTMLButtonElement>(
|
||||
'.exit-fullscreen-button',
|
||||
);
|
||||
const getPlayMinimizeButton = () =>
|
||||
document.querySelector('.player-minimize-button');
|
||||
const appLayout = document.querySelector<HTMLElement>('ytmusic-app-layout');
|
||||
const expandMenu = document.querySelector<HTMLElement>('#expanding-menu');
|
||||
const middleControls =
|
||||
document.querySelector<HTMLButtonElement>('.middle-controls');
|
||||
const playerPage = document.querySelector<
|
||||
HTMLElement & { playerPageOpen_: boolean }
|
||||
>('ytmusic-player-page');
|
||||
const togglePlayerPageButton = document.querySelector<HTMLButtonElement>(
|
||||
'.toggle-player-page-button',
|
||||
);
|
||||
const fullScreenButton =
|
||||
document.querySelector<HTMLButtonElement>('.fullscreen-button');
|
||||
const player = document.querySelector<
|
||||
HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }
|
||||
>('#player');
|
||||
const onPlayerDblClick = player?.onDoubleClick_;
|
||||
const mouseLeaveEventListener = () => middleControls?.click();
|
||||
|
||||
const titleBar = document.querySelector<HTMLElement>(
|
||||
'nav[data-ytmd-main-panel]',
|
||||
);
|
||||
|
||||
const pipClickEventListener = async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await togglePictureInPicture();
|
||||
};
|
||||
|
||||
ipc.on('ytmd:pip-toggle', (isPip: boolean) => {
|
||||
if (exitFullScreenButton && player) {
|
||||
if (isPip) {
|
||||
exitFullScreenButton?.addEventListener('click', pipClickEventListener);
|
||||
getPlayMinimizeButton()?.removeEventListener(
|
||||
'click',
|
||||
pipClickEventListener,
|
||||
);
|
||||
player.onDoubleClick_ = () => {};
|
||||
|
||||
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
|
||||
if (!playerPage?.playerPageOpen_) {
|
||||
togglePlayerPageButton?.click();
|
||||
}
|
||||
|
||||
fullScreenButton?.click();
|
||||
appLayout?.classList.add('pip');
|
||||
if (titleBar) {
|
||||
titleBar.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
exitFullScreenButton.removeEventListener(
|
||||
'click',
|
||||
pipClickEventListener,
|
||||
);
|
||||
getPlayMinimizeButton()?.addEventListener(
|
||||
'click',
|
||||
pipClickEventListener,
|
||||
);
|
||||
player.onDoubleClick_ = onPlayerDblClick;
|
||||
expandMenu?.removeEventListener('mouseleave', mouseLeaveEventListener);
|
||||
exitFullScreenButton.click();
|
||||
appLayout?.classList.remove('pip');
|
||||
if (titleBar) {
|
||||
titleBar.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getPlayMinimizeButton()?.addEventListener('click', pipClickEventListener);
|
||||
|
||||
const pipButtonContainer = document.createElement('div');
|
||||
pipButtonContainer.classList.add(
|
||||
'style-scope',
|
||||
'menu-item',
|
||||
'ytmusic-menu-popup-renderer',
|
||||
);
|
||||
pipButtonContainer.setAttribute('aria-disabled', 'false');
|
||||
pipButtonContainer.setAttribute('aria-selected', 'false');
|
||||
pipButtonContainer.setAttribute('role', 'option');
|
||||
pipButtonContainer.setAttribute('tabindex', '-1');
|
||||
|
||||
render(
|
||||
() => (
|
||||
<PictureInPictureButton
|
||||
onClick={togglePictureInPicture}
|
||||
text={t('plugins.picture-in-picture.templates.button')}
|
||||
/>
|
||||
),
|
||||
pipButtonContainer,
|
||||
);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const menu = getSongMenu();
|
||||
|
||||
if (menu?.contains(pipButtonContainer) || !isMusicOrVideoTrack()) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu?.prepend(pipButtonContainer);
|
||||
});
|
||||
|
||||
observer.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
export interface PictureInPictureButtonProps {
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const PictureInPictureButton = (props: PictureInPictureButtonProps) => (
|
||||
<a
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="navigation-endpoint"
|
||||
tabindex={-1}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||
<svg
|
||||
class="style-scope yt-icon"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
id="Layer_1"
|
||||
viewBox="0 0 512 512"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px"
|
||||
>
|
||||
<g class="style-scope yt-icon" id="XMLID_6_">
|
||||
<path
|
||||
class="style-scope yt-icon"
|
||||
fill="#aaaaaa"
|
||||
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||
v326.8H464.8z"
|
||||
id="XMLID_11_"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="ytmcustom-pip"
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
@ -1,52 +0,0 @@
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||
onclick="togglePictureInPicture()"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="navigation-endpoint"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
|
||||
>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
style="enable-background: new 0 0 512 512"
|
||||
version="1.1"
|
||||
viewBox="0 0 512 512"
|
||||
x="0px"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #aaaaaa;
|
||||
}
|
||||
</style>
|
||||
<g id="XMLID_6_">
|
||||
<path
|
||||
class="st0"
|
||||
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||
v326.8H464.8z"
|
||||
id="XMLID_11_"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="ytmcustom-pip"
|
||||
>
|
||||
<ytmd-trans
|
||||
key="plugins.picture-in-picture.templates.button"
|
||||
></ytmd-trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user