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

@ -7,7 +7,7 @@ import { t } from '@/i18n';
export default createBackend({
start({ ipc: { handle }, window }) {
handle(
'captionsSelector',
'ytmd:captions-selector',
async (captionLabels: Record<string, string>, currentIndex: string) =>
await prompt(
{

View File

@ -1,21 +1,20 @@
import { createPlugin } from '@/utils';
import { YoutubePlayer } from '@/types/youtube-player';
import { t } from '@/i18n';
import backend from './back';
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
import { t } from '@/i18n';
import type { YoutubePlayer } from '@/types/youtube-player';
export default createPlugin<
unknown,
unknown,
{
captionsSettingsButton: HTMLElement;
captionsSettingsButton?: HTMLElement;
captionTrackList: LanguageOptions[] | null;
api: YoutubePlayer | null;
config: CaptionsSelectorConfig | null;
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
videoChangeListener: () => void;
captionsButtonClickListener: () => void;
},
CaptionsSelectorConfig
>({

View File

@ -1,151 +0,0 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { createRenderer } from '@/utils';
import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html?raw';
import { YoutubePlayer } from '@/types/youtube-player';
export interface LanguageOptions {
displayName: string;
id: string | null;
is_default: boolean;
is_servable: boolean;
is_translateable: boolean;
kind: string;
languageCode: string; // 2 length
languageName: string;
name: string | null;
vss_id: string;
}
export interface CaptionsSelectorConfig {
enabled: boolean;
disableCaptions: boolean;
autoload: boolean;
lastCaptionsCode: string;
}
export default createRenderer<
{
captionsSettingsButton: HTMLElement;
captionTrackList: LanguageOptions[] | null;
api: YoutubePlayer | null;
config: CaptionsSelectorConfig | null;
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
videoChangeListener: () => void;
captionsButtonClickListener: () => void;
},
CaptionsSelectorConfig
>({
captionsSettingsButton: ElementFromHtml(CaptionsSettingsButtonHTML),
captionTrackList: null,
api: null,
config: null,
setConfig: () => {},
async captionsButtonClickListener() {
if (this.captionTrackList?.length) {
const currentCaptionTrack = this.api!.getOption<LanguageOptions>(
'captions',
'track',
);
let currentIndex = currentCaptionTrack
? this.captionTrackList.indexOf(
this.captionTrackList.find(
(track) =>
track.languageCode === currentCaptionTrack.languageCode,
)!,
)
: null;
const captionLabels = [
...this.captionTrackList.map((track) => track.displayName),
'None',
];
currentIndex = (await window.ipcRenderer.invoke(
'captionsSelector',
captionLabels,
currentIndex,
)) as number;
if (currentIndex === null) {
return;
}
const newCaptions = this.captionTrackList[currentIndex];
this.setConfig({ lastCaptionsCode: newCaptions?.languageCode });
if (newCaptions) {
this.api?.setOption('captions', 'track', {
languageCode: newCaptions.languageCode,
});
} else {
this.api?.setOption('captions', 'track', {});
}
setTimeout(() => this.api?.playVideo());
}
},
videoChangeListener() {
if (this.config?.disableCaptions) {
setTimeout(() => this.api!.unloadModule('captions'), 100);
this.captionsSettingsButton.style.display = 'none';
return;
}
this.api!.loadModule('captions');
setTimeout(() => {
this.captionTrackList =
this.api!.getOption('captions', 'tracklist') ?? [];
if (this.config!.autoload && this.config!.lastCaptionsCode) {
this.api?.setOption('captions', 'track', {
languageCode: this.config!.lastCaptionsCode,
});
}
this.captionsSettingsButton.style.display = this.captionTrackList?.length
? 'inline-block'
: 'none';
}, 250);
},
async start({ getConfig, setConfig }) {
this.config = await getConfig();
this.setConfig = setConfig;
},
stop() {
document
.querySelector('.right-controls-buttons')
?.removeChild(this.captionsSettingsButton);
document
.querySelector<YoutubePlayer & HTMLElement>('#movie_player')
?.unloadModule('captions');
document
.querySelector('video')
?.removeEventListener('ytmd:src-changed', this.videoChangeListener);
this.captionsSettingsButton.removeEventListener(
'click',
this.captionsButtonClickListener,
);
},
onPlayerApiReady(playerApi) {
this.api = playerApi;
document
.querySelector('.right-controls-buttons')
?.append(this.captionsSettingsButton);
this.captionTrackList =
this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
document
.querySelector('video')
?.addEventListener('ytmd:src-changed', this.videoChangeListener);
this.captionsSettingsButton.addEventListener(
'click',
this.captionsButtonClickListener,
);
},
onConfigChange(newConfig) {
this.config = newConfig;
},
});

View File

@ -0,0 +1,164 @@
import { render } from 'solid-js/web';
import { createSignal, Show } from 'solid-js';
import { createRenderer } from '@/utils';
import { t } from '@/i18n';
import { CaptionsSettingButton } from './templates/captions-settings-template';
import type { YoutubePlayer } from '@/types/youtube-player';
import type { AppElement } from '@/types/queue';
export interface LanguageOptions {
displayName: string;
id: string | null;
is_default: boolean;
is_servable: boolean;
is_translateable: boolean;
kind: string;
languageCode: string; // 2 length
languageName: string;
name: string | null;
vss_id: string;
}
export interface CaptionsSelectorConfig {
enabled: boolean;
disableCaptions: boolean;
autoload: boolean;
lastCaptionsCode: string;
}
const [hidden, setHidden] = createSignal(false);
export default createRenderer<
{
captionsSettingsButton?: HTMLElement;
captionTrackList: LanguageOptions[] | null;
api: YoutubePlayer | null;
config: CaptionsSelectorConfig | null;
videoChangeListener: () => void;
},
CaptionsSelectorConfig
>({
captionTrackList: null,
api: null,
config: null,
videoChangeListener() {
if (this.config?.disableCaptions) {
setTimeout(() => this.api!.unloadModule('captions'), 100);
setHidden(true);
return;
}
this.api!.loadModule('captions');
setTimeout(() => {
this.captionTrackList =
this.api!.getOption('captions', 'tracklist') ?? [];
if (this.config!.autoload && this.config!.lastCaptionsCode) {
this.api?.setOption('captions', 'track', {
languageCode: this.config!.lastCaptionsCode,
});
}
setHidden(!this.captionTrackList?.length);
}, 250);
},
async start({ getConfig }) {
this.config = await getConfig();
},
stop() {
this.api?.unloadModule('captions');
document
.querySelector('video')
?.removeEventListener('ytmd:src-changed', this.videoChangeListener);
if (this.captionsSettingsButton) {
document
.querySelector('.right-controls-buttons')
?.removeChild(this.captionsSettingsButton);
}
},
onPlayerApiReady(playerApi, { ipc, setConfig }) {
this.api = playerApi;
render(
() => (
<Show when={!hidden()}>
<CaptionsSettingButton
label={t('plugins.captions-selector.templates.title')}
onClick={async () => {
const appApi = document.querySelector<AppElement>('ytmusic-app');
if (this.captionTrackList?.length) {
const currentCaptionTrack =
playerApi.getOption<LanguageOptions>('captions', 'track');
let currentIndex = currentCaptionTrack
? this.captionTrackList.indexOf(
this.captionTrackList.find(
(track) =>
track.languageCode ===
currentCaptionTrack.languageCode,
)!,
)
: null;
const captionLabels = [
...this.captionTrackList.map((track) => track.displayName),
'None',
];
currentIndex = (await ipc.invoke(
'ytmd:captions-selector',
captionLabels,
currentIndex,
)) as number;
if (currentIndex === null) {
return;
}
const newCaptions = this.captionTrackList[currentIndex];
setConfig({ lastCaptionsCode: newCaptions?.languageCode });
if (newCaptions) {
playerApi.setOption('captions', 'track', {
languageCode: newCaptions.languageCode,
});
appApi?.toastService?.show(
t('plugins.captions-selector.toast.caption-changed', {
language: newCaptions.displayName,
}),
);
} else {
playerApi.setOption('captions', 'track', {});
appApi?.toastService?.show(
t('plugins.captions-selector.toast.caption-disabled'),
);
}
setTimeout(() => playerApi.playVideo());
} else {
appApi?.toastService?.show(
t('plugins.captions-selector.toast.no-captions'),
);
}
}}
ref={this.captionsSettingsButton}
/>
</Show>
),
document.querySelector('.right-controls-buttons')!,
);
this.captionTrackList =
this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
document
.querySelector('video')
?.addEventListener('ytmd:src-changed', this.videoChangeListener);
},
onConfigChange(newConfig) {
this.config = newConfig;
},
});

View File

@ -1,26 +0,0 @@
<tp-yt-paper-icon-button
aria-disabled="false"
aria-label="Open captions selector"
class="player-captions-button style-scope ytmusic-player"
icon="yt-icons:subtitles"
role="button"
tabindex="0"
title="Open captions selector"
>
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
<svg
class="style-scope yt-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon">
<path
class="style-scope tp-yt-iron-icon"
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
></path>
</g>
</svg>
</tp-yt-iron-icon>
</tp-yt-paper-icon-button>

View File

@ -0,0 +1,42 @@
export interface CaptionsSettingsButtonProps {
label: string;
onClick: (event: MouseEvent) => void;
}
export const CaptionsSettingButton = (props: CaptionsSettingsButtonProps) => (
<yt-icon-button
aria-disabled={false}
aria-label={props.label}
class="player-captions-button style-scope ytmusic-player-bar"
icon={'yt-icons:subtitles'}
role={'button'}
tabindex={0}
title={props.label}
on:click={props.onClick}
>
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
<div
style={{
width: '100%',
height: '100%',
display: 'block',
fill: 'currentcolor',
}}
>
<svg
class="style-scope yt-icon"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon">
<path
class="style-scope yt-icon"
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
></path>
</g>
</svg>
</div>
</span>
</yt-icon-button>
);