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

@ -0,0 +1,92 @@
export interface PlaybackSpeedSliderProps {
speed: number;
title: string;
onImmediateValueChanged?: (value: CustomEvent<{ value: number }>) => void;
onWheel?: (event: WheelEvent) => void;
}
export const PlaybackSpeedSlider = (props: PlaybackSpeedSliderProps) => (
<div
aria-disabled="false"
aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
>
<div
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
id="navigation-endpoint"
tabindex="-1"
>
<tp-yt-paper-slider
aria-disabled="false"
aria-label={props.title}
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow={props.speed}
class="volume-slider style-scope ytmusic-player-bar on-hover"
dir="ltr"
on:immediate-value-changed={props.onImmediateValueChanged}
onWheel={props.onWheel}
max="2"
min="0"
role="slider"
step="0.125"
style="display: inherit !important"
tabindex="0"
title={props.title}
value={props.speed}
>
<div class="style-scope tp-yt-paper-slider" id="sliderContainer">
<div class="bar-container style-scope tp-yt-paper-slider">
<tp-yt-paper-progress
aria-disabled="false"
aria-hidden="true"
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow="1"
class="style-scope tp-yt-paper-slider"
id="sliderBar"
role="progressbar"
style="touch-action: none"
value="1"
>
<div
class="style-scope tp-yt-paper-progress"
id="progressContainer"
>
<div
class="style-scope tp-yt-paper-progress"
hidden={true}
id="secondaryProgress"
style="transform: scaleX(0)"
/>
<div
class="style-scope tp-yt-paper-progress"
id="primaryProgress"
style="transform: scaleX(0.5)"
/>
</div>
</tp-yt-paper-progress>
</div>
<div
class="slider-knob style-scope tp-yt-paper-slider"
id="sliderKnob"
style="left: 50%; touch-action: none"
>
<input
class="slider-knob-inner style-scope tp-yt-paper-slider"
value={1}
/>
</div>
</div>
</tp-yt-paper-slider>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-playback-speed"
>
{props.title} ({props.speed})
</div>
</div>
</div>
);

View File

@ -1,141 +0,0 @@
import sliderHTML from './templates/slider.html?raw';
import { getSongMenu } from '@/providers/dom-elements';
import { singleton } from '@/providers/decorators';
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
import { ElementFromHtml } from '../utils/renderer';
const slider = ElementFromHtml(sliderHTML);
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
const MIN_PLAYBACK_SPEED = 0.07;
const MAX_PLAYBACK_SPEED = 16;
let playbackSpeed = 1;
const updatePlayBackSpeed = () => {
const videoElement = document.querySelector<HTMLVideoElement>('video');
if (videoElement) {
videoElement.playbackRate = playbackSpeed;
}
const playbackSpeedElement = document.querySelector('#playback-speed-value');
if (playbackSpeedElement) {
const targetHtml = String(playbackSpeed);
(playbackSpeedElement.innerHTML as string | TrustedHTML) =
defaultTrustedTypePolicy
? defaultTrustedTypePolicy.createHTML(targetHtml)
: targetHtml;
}
};
let menu: Element | null = null;
const immediateValueChangedListener = (e: Event) => {
playbackSpeed =
(e as CustomEvent<{ value: number }>).detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
updatePlayBackSpeed();
};
const setupSliderListener = singleton(() => {
document
.querySelector('#playback-speed-slider')
?.addEventListener(
'immediate-value-changed',
immediateValueChangedListener,
);
});
const observePopupContainer = () => {
const observer = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
}
if (menu && !menu.contains(slider)) {
menu.prepend(slider);
setupSliderListener();
}
});
const popupContainer = document.querySelector('ytmusic-popup-container');
if (popupContainer) {
observer.observe(popupContainer, {
childList: true,
subtree: true,
});
}
};
const observeVideo = () => {
const video = document.querySelector<HTMLVideoElement>('video');
if (video) {
video.addEventListener('ratechange', forcePlaybackRate);
video.addEventListener('ytmd:src-changed', forcePlaybackRate);
}
};
const wheelEventListener = (e: WheelEvent) => {
e.preventDefault();
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
// E.deltaY < 0 means wheel-up
playbackSpeed = roundToTwo(
e.deltaY < 0
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
);
updatePlayBackSpeed();
// Update slider position
const playbackSpeedSilder = document.querySelector<
HTMLElement & { value: number }
>('#playback-speed-slider');
if (playbackSpeedSilder) {
playbackSpeedSilder.value = playbackSpeed;
}
};
const setupWheelListener = () => {
slider.addEventListener('wheel', wheelEventListener);
};
function forcePlaybackRate(e: Event) {
if (e.target instanceof HTMLVideoElement) {
const videoElement = e.target;
if (videoElement.playbackRate !== playbackSpeed) {
videoElement.playbackRate = playbackSpeed;
}
}
}
export const onPlayerApiReady = () => {
observePopupContainer();
observeVideo();
setupWheelListener();
};
export const onUnload = () => {
const video = document.querySelector<HTMLVideoElement>('video');
if (video) {
video.removeEventListener('ratechange', forcePlaybackRate);
video.removeEventListener('ytmd:src-changed', forcePlaybackRate);
}
slider.removeEventListener('wheel', wheelEventListener);
getSongMenu()?.removeChild(slider);
document
.querySelector('#playback-speed-slider')
?.removeEventListener(
'immediate-value-changed',
immediateValueChangedListener,
);
};

View File

@ -0,0 +1,119 @@
import { render } from 'solid-js/web';
import { createSignal } from 'solid-js';
import { getSongMenu } from '@/providers/dom-elements';
import { PlaybackSpeedSlider } from './components/slider';
import { t } from '@/i18n';
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
const MIN_PLAYBACK_SPEED = 0.07;
const MAX_PLAYBACK_SPEED = 16;
const forcePlaybackRate = (e: Event) => {
if (e.target instanceof HTMLVideoElement) {
const videoElement = e.target;
if (videoElement.playbackRate !== speed()) {
videoElement.playbackRate = speed();
}
}
};
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
const [speed, setSpeed] = createSignal(1);
const sliderContainer = document.createElement('div');
export const onPlayerApiReady = () => {
const observePopupContainer = () => {
const updatePlayBackSpeed = () => {
const videoElement = document.querySelector<HTMLVideoElement>('video');
if (videoElement) {
videoElement.playbackRate = speed();
}
setSpeed(speed());
};
render(
() => (
<PlaybackSpeedSlider
speed={speed()}
title={t('plugins.playback-speed.templates.button')}
onImmediateValueChanged={(e) => {
let targetSpeed = Number(e.detail.value ?? MIN_PLAYBACK_SPEED);
if (isNaN(targetSpeed)) {
targetSpeed = 1;
}
targetSpeed = Math.min(
Math.max(MIN_PLAYBACK_SPEED, targetSpeed),
MAX_PLAYBACK_SPEED,
);
setSpeed(targetSpeed);
updatePlayBackSpeed();
}}
onWheel={(e) => {
e.preventDefault();
if (isNaN(speed())) {
setSpeed(1);
}
// E.deltaY < 0 means wheel-up
setSpeed((prev) =>
roundToTwo(
e.deltaY < 0
? Math.min(prev + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(prev - 0.01, MIN_PLAYBACK_SPEED),
),
);
updatePlayBackSpeed();
}}
/>
),
sliderContainer,
);
const observer = new MutationObserver(() => {
const menu = getSongMenu();
if (menu && !menu.contains(sliderContainer) && isMusicOrVideoTrack()) {
menu.prepend(sliderContainer);
}
});
const popupContainer = document.querySelector('ytmusic-popup-container');
if (popupContainer) {
observer.observe(popupContainer, {
childList: true,
subtree: true,
});
}
};
const observeVideo = () => {
const video = document.querySelector<HTMLVideoElement>('video');
if (video) {
video.addEventListener('ratechange', forcePlaybackRate);
video.addEventListener('ytmd:src-changed', forcePlaybackRate);
}
};
observePopupContainer();
observeVideo();
};
export const onUnload = () => {
const video = document.querySelector<HTMLVideoElement>('video');
if (video) {
video.removeEventListener('ratechange', forcePlaybackRate);
video.removeEventListener('ytmd:src-changed', forcePlaybackRate);
}
getSongMenu()?.removeChild(sliderContainer);
};

View File

@ -1,90 +0,0 @@
<div
aria-disabled="false"
aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
>
<div
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
id="navigation-endpoint"
tabindex="-1"
>
<tp-yt-paper-slider
aria-disabled="false"
aria-label="Playback speed"
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow="1"
class="volume-slider style-scope ytmusic-player-bar on-hover"
dir="ltr"
id="playback-speed-slider"
max="2"
min="0"
role="slider"
step="0.125"
style="display: inherit !important"
tabindex="0"
title="Playback speed"
value="1"
><!--css-build:shady-->
<div class="style-scope tp-yt-paper-slider" id="sliderContainer">
<div class="bar-container style-scope tp-yt-paper-slider">
<tp-yt-paper-progress
aria-disabled="false"
aria-hidden="true"
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow="1"
class="style-scope tp-yt-paper-slider"
id="sliderBar"
role="progressbar"
style="touch-action: none"
value="1"
><!--css-build:shady-->
<div
class="style-scope tp-yt-paper-progress"
id="progressContainer"
>
<div
class="style-scope tp-yt-paper-progress"
hidden="true"
id="secondaryProgress"
style="transform: scaleX(0)"
></div>
<div
class="style-scope tp-yt-paper-progress"
id="primaryProgress"
style="transform: scaleX(0.5)"
></div>
</div>
</tp-yt-paper-progress>
</div>
<dom-if class="style-scope tp-yt-paper-slider">
<template is="dom-if"></template>
</dom-if>
<div
class="slider-knob style-scope tp-yt-paper-slider"
id="sliderKnob"
style="left: 50%; touch-action: none"
>
<div
class="slider-knob-inner style-scope tp-yt-paper-slider"
value="1"
></div>
</div>
</div>
<dom-if class="style-scope tp-yt-paper-slider">
<template is="dom-if"></template>
</dom-if>
</tp-yt-paper-slider>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-playback-speed"
>
<ytmd-trans key="plugins.playback-speed.templates.button"></ytmd-trans>
(<span id="playback-speed-value">1</span>)
</div>
</div>
</div>