diff --git a/src/plugins/captions-selector/back.ts b/src/plugins/captions-selector/back.ts new file mode 100644 index 00000000..573e77c2 --- /dev/null +++ b/src/plugins/captions-selector/back.ts @@ -0,0 +1,28 @@ +import prompt from 'custom-electron-prompt'; + +import promptOptions from '@/providers/prompt-options'; +import { createBackend } from '@/utils'; + +export default createBackend({ + start({ ipc: { handle }, window }) { + handle( + 'captionsSelector', + async (captionLabels: Record, currentIndex: string) => + await prompt( + { + title: 'Choose Caption', + label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`, + type: 'select', + value: currentIndex, + selectOptions: captionLabels, + resizable: true, + ...promptOptions(), + }, + window, + ), + ); + }, + stop({ ipc: { removeHandler } }) { + removeHandler('captionsSelector'); + }, +}); diff --git a/src/plugins/captions-selector/index.ts b/src/plugins/captions-selector/index.ts index 10fc77ea..7c590571 100644 --- a/src/plugins/captions-selector/index.ts +++ b/src/plugins/captions-selector/index.ts @@ -1,32 +1,8 @@ -import prompt from 'custom-electron-prompt'; - -import promptOptions from '@/providers/prompt-options'; import { createPlugin } from '@/utils'; -import { ElementFromHtml } from '@/plugins/utils/renderer'; - -import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html?raw'; - import { YoutubePlayer } from '@/types/youtube-player'; -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; -} - -interface CaptionsSelectorConfig { - enabled: boolean; - disableCaptions: boolean; - autoload: boolean; - lastCaptionsCode: string; -} +import backend from './back'; +import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer'; export default createPlugin< unknown, @@ -36,9 +12,9 @@ export default createPlugin< captionTrackList: LanguageOptions[] | null; api: YoutubePlayer | null; config: CaptionsSelectorConfig | null; - setConfig: ((config: Partial) => void); - videoChangeListener: (() => void); - captionsButtonClickListener: (() => void); + setConfig: (config: Partial) => void; + videoChangeListener: () => void; + captionsButtonClickListener: () => void; }, CaptionsSelectorConfig >({ @@ -72,109 +48,6 @@ export default createPlugin< ]; }, - backend: { - start({ ipc: { handle }, window }) { - handle( - 'captionsSelector', - async (captionLabels: Record, currentIndex: string) => - await prompt( - { - title: 'Choose Caption', - label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`, - type: 'select', - value: currentIndex, - selectOptions: captionLabels, - resizable: true, - ...promptOptions(), - }, - window, - ), - ); - }, - stop({ ipc: { removeHandler } }) { - removeHandler('captionsSelector'); - } - }, - - renderer: { - captionsSettingsButton: ElementFromHtml(CaptionsSettingsButtonHTML), - captionTrackList: null, - api: null, - config: null, - setConfig: () => {}, - async captionsButtonClickListener() { - if (this.captionTrackList?.length) { - const currentCaptionTrack = this.api!.getOption('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('#movie_player')?.unloadModule('captions'); - document.querySelector('video')?.removeEventListener('srcChanged', 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('captions', 'tracklist') ?? []; - - document.querySelector('video')?.addEventListener('srcChanged', this.videoChangeListener); - this.captionsSettingsButton.addEventListener('click', this.captionsButtonClickListener); - }, - onConfigChange(newConfig) { - this.config = newConfig; - }, - }, + backend, + renderer, }); diff --git a/src/plugins/captions-selector/renderer.ts b/src/plugins/captions-selector/renderer.ts new file mode 100644 index 00000000..cce97f6e --- /dev/null +++ b/src/plugins/captions-selector/renderer.ts @@ -0,0 +1,151 @@ +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) => 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( + '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('#movie_player') + ?.unloadModule('captions'); + document + .querySelector('video') + ?.removeEventListener('srcChanged', 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('captions', 'tracklist') ?? []; + + document + .querySelector('video') + ?.addEventListener('srcChanged', this.videoChangeListener); + this.captionsSettingsButton.addEventListener( + 'click', + this.captionsButtonClickListener, + ); + }, + onConfigChange(newConfig) { + this.config = newConfig; + }, +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 1937596a..61c1a09d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,6 +9,8 @@ import type { PluginConfig, PluginLifecycleExtra, PluginLifecycleSimple, + PluginLifecycle, + RendererPluginLifecycle, } from '@/types/plugins'; export const createPlugin = < @@ -29,6 +31,37 @@ export const createPlugin = < }, ) => def; +export const createBackend = < + BackendProperties, + Config extends PluginConfig = PluginConfig, +>( + back: { + [Key in keyof BackendProperties]: BackendProperties[Key]; + } & PluginLifecycle, BackendProperties>, +) => back; + +export const createPreload = < + PreloadProperties, + Config extends PluginConfig = PluginConfig, +>( + preload: { + [Key in keyof PreloadProperties]: PreloadProperties[Key]; + } & PluginLifecycle, PreloadProperties>, +) => preload; + +export const createRenderer = < + RendererProperties, + Config extends PluginConfig = PluginConfig, +>( + renderer: { + [Key in keyof RendererProperties]: RendererProperties[Key]; + } & RendererPluginLifecycle< + Config, + RendererContext, + RendererProperties + >, +) => renderer; + type Options = | { ctx: 'backend'; context: BackendContext } | { ctx: 'preload'; context: PreloadContext }