mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 19:01: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:
@ -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(
|
||||
{
|
||||
|
||||
@ -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
|
||||
>({
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
164
src/plugins/captions-selector/renderer.tsx
Normal file
164
src/plugins/captions-selector/renderer.tsx
Normal 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;
|
||||
},
|
||||
});
|
||||
@ -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>
|
||||
@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user