feat: migrate from raw HTML to JSX (TSX / SolidJS) (#3583)

Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2025-07-09 23:14:11 +09:00
committed by GitHub
parent 9ccd126eee
commit e114e0ef44
90 changed files with 1723 additions and 1357 deletions

View File

@ -1,26 +1,24 @@
import downloadHTML from './templates/download.html?raw';
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import defaultConfig from '@/config/defaults';
import { getSongMenu } from '@/providers/dom-elements';
import { getSongInfo } from '@/providers/song-info-front';
import { LoggerPrefix } from '@/utils';
import { t } from '@/i18n';
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
import { ElementFromHtml } from '../utils/renderer';
import { DownloadButton } from './templates/download';
import type { RendererContext } from '@/types/contexts';
import type { DownloaderPluginConfig } from './index';
let menu: Element | null = null;
let progress: Element | null = null;
const downloadButton = ElementFromHtml(downloadHTML);
let menu: HTMLElement | null = null;
let download: () => void;
let doneFirstLoad = false;
const [downloadButtonText, setDownloadButtonText] = createSignal<string>('');
let buttonContainer: HTMLDivElement | null = null;
const menuObserver = new MutationObserver(() => {
if (!menu) {
@ -30,46 +28,24 @@ const menuObserver = new MutationObserver(() => {
}
}
if (menu.contains(downloadButton)) {
if (
menu.contains(buttonContainer) ||
!isMusicOrVideoTrack() ||
!buttonContainer
) {
return;
}
// check for video (or music)
let menuUrl = document.querySelector<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(downloadButton);
progress = document.querySelector('#ytmcustom-download');
if (!doneFirstLoad) {
setTimeout(() => (doneFirstLoad ||= true), 500);
}
menu.prepend(buttonContainer);
});
export const onRendererLoad = ({
ipc,
}: RendererContext<DownloaderPluginConfig>) => {
window.download = () => {
download = () => {
const songMenu = getSongMenu();
let videoUrl = songMenu
// Selector of first button which is always "Start Radio"
?.querySelector(
'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
)
@ -108,21 +84,30 @@ export const onRendererLoad = ({
};
ipc.on('downloader-feedback', (feedback: string) => {
if (progress) {
const targetHtml = feedback || t('plugins.downloader.templates.button');
(progress.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
? defaultTrustedTypePolicy.createHTML(targetHtml)
: targetHtml;
} else {
console.warn(
LoggerPrefix,
t('plugins.downloader.renderer.can-not-update-progress'),
);
}
const targetHtml = feedback || t('plugins.downloader.templates.button');
setDownloadButtonText(targetHtml);
});
};
export const onPlayerApiReady = () => {
setDownloadButtonText(t('plugins.downloader.templates.button'));
buttonContainer = document.createElement('div');
buttonContainer.classList.add(
'style-scope',
'menu-item',
'ytmusic-menu-popup-renderer',
);
buttonContainer.setAttribute('aria-disabled', 'false');
buttonContainer.setAttribute('aria-selected', 'false');
buttonContainer.setAttribute('role', 'option');
buttonContainer.setAttribute('tabindex', '-1');
render(
() => <DownloadButton onClick={download} text={downloadButtonText()} />,
buttonContainer,
);
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
childList: true,
subtree: true,

View File

@ -1,24 +1,23 @@
<div
aria-disabled="false"
aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
onclick="download()"
role="option"
tabindex="-1"
>
export const DownloadButton = (props: {
onClick: () => void;
text: string;
}) => (
<a
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
id="navigation-endpoint"
tabindex="-1"
tabindex={-1}
onClick={props.onClick}
>
<div
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
>
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
<svg
class="style-scope yt-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
style={{
'pointer-events': 'none',
'display': 'block',
'width': '100%',
'height': '100%',
}}
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon">
@ -39,7 +38,7 @@
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-download"
>
<ytmd-trans key="plugins.downloader.templates.button"></ytmd-trans>
{props.text}
</div>
</a>
</div>
);