mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 11:21:46 +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:
92
src/plugins/playback-speed/components/slider.tsx
Normal file
92
src/plugins/playback-speed/components/slider.tsx
Normal 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>
|
||||
);
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
119
src/plugins/playback-speed/renderer.tsx
Normal file
119
src/plugins/playback-speed/renderer.tsx
Normal 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);
|
||||
};
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user