From d30755e5fa9530c5769b6265d68f16d17210678e Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Sun, 3 Sep 2023 06:37:47 +0900 Subject: [PATCH] feat: migration to TypeScript part 2 Co-authored-by: Su-Yong --- config/defaults.ts | 3 + config/dynamic.ts | 19 ++-- custom-electron-prompt.d.ts | 12 ++- navigation.d.ts | 88 +++++++++++++++++++ package-lock.json | 7 ++ package.json | 1 + plugins/adblocker/config.ts | 3 + plugins/adblocker/menu.ts | 10 +-- plugins/audio-compressor/front.js | 19 ---- plugins/audio-compressor/front.ts | 17 ++++ plugins/blur-nav-bar/back.js | 7 -- plugins/blur-nav-bar/back.ts | 9 ++ .../{front.js => front.ts} | 2 +- plugins/captions-selector/back.js | 19 ---- plugins/captions-selector/back.ts | 19 ++++ plugins/captions-selector/config.js | 4 - plugins/captions-selector/config.ts | 4 + .../captions-selector/{front.js => front.ts} | 47 ++++++---- .../captions-selector/{menu.js => menu.ts} | 6 +- .../compact-sidebar/{front.js => front.ts} | 4 +- plugins/crossfade/back.js | 15 ---- plugins/crossfade/back.ts | 15 ++++ plugins/crossfade/config.js | 4 - plugins/crossfade/config.ts | 4 + plugins/crossfade/{fader.js => fader.ts} | 81 ++++++++++++----- plugins/crossfade/{front.js => front.ts} | 71 +++++++-------- plugins/crossfade/{menu.js => menu.ts} | 27 +++--- plugins/disable-autoplay/front.js | 14 --- plugins/disable-autoplay/front.ts | 14 +++ plugins/discord/{back.js => back.ts} | 83 ++++++++--------- plugins/discord/{menu.js => menu.ts} | 34 ++++--- plugins/skip-silences/{front.js => front.ts} | 31 ++++--- .../taskbar-mediacontrol/{back.js => back.ts} | 35 +++++--- plugins/touchbar/{back.js => back.ts} | 20 +++-- plugins/tuna-obs/{back.js => back.ts} | 38 +++++--- plugins/utils.ts | 5 +- providers/song-info.ts | 2 +- reset.d.ts | 19 ++++ tsconfig.json | 1 + types/youtube-player.ts | 6 +- 40 files changed, 523 insertions(+), 296 deletions(-) create mode 100644 navigation.d.ts delete mode 100644 plugins/audio-compressor/front.js create mode 100644 plugins/audio-compressor/front.ts delete mode 100644 plugins/blur-nav-bar/back.js create mode 100644 plugins/blur-nav-bar/back.ts rename plugins/bypass-age-restrictions/{front.js => front.ts} (88%) delete mode 100644 plugins/captions-selector/back.js create mode 100644 plugins/captions-selector/back.ts delete mode 100644 plugins/captions-selector/config.js create mode 100644 plugins/captions-selector/config.ts rename plugins/captions-selector/{front.js => front.ts} (58%) rename plugins/captions-selector/{menu.js => menu.ts} (77%) rename plugins/compact-sidebar/{front.js => front.ts} (71%) delete mode 100644 plugins/crossfade/back.js create mode 100644 plugins/crossfade/back.ts delete mode 100644 plugins/crossfade/config.js create mode 100644 plugins/crossfade/config.ts rename plugins/crossfade/{fader.js => fader.ts} (83%) rename plugins/crossfade/{front.js => front.ts} (59%) rename plugins/crossfade/{menu.js => menu.ts} (70%) delete mode 100644 plugins/disable-autoplay/front.js create mode 100644 plugins/disable-autoplay/front.ts rename plugins/discord/{back.js => back.ts} (68%) rename plugins/discord/{menu.js => menu.ts} (60%) rename plugins/skip-silences/{front.js => front.ts} (77%) rename plugins/taskbar-mediacontrol/{back.js => back.ts} (54%) rename plugins/touchbar/{back.js => back.ts} (82%) rename plugins/tuna-obs/{back.js => back.ts} (55%) diff --git a/config/defaults.ts b/config/defaults.ts index 2d29ef29..c454863a 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -122,6 +122,9 @@ const defaultConfig = { 'captions-selector': { enabled: false, disableCaptions: false, + autoload: false, + lastCaptionsCode: '', + disabledCaptions: false, }, 'skip-silences': { onlySkipBeginning: false, diff --git a/config/dynamic.ts b/config/dynamic.ts index 0e4faa07..01134b53 100644 --- a/config/dynamic.ts +++ b/config/dynamic.ts @@ -70,13 +70,14 @@ interface PluginConfigOptions { * setupMyPlugin(win, config); * }; */ -type ConfigType = typeof defaultConfig.plugins[T]; +export type ConfigType = typeof defaultConfig.plugins[T]; type ValueOf = T[keyof T]; +type Mode = Mode extends 'r' ? Promise : T; export class PluginConfig { - private name: string; - private config: ConfigType; - private defaultConfig: ConfigType; - private enableFront: boolean; + private readonly name: string; + private readonly config: ConfigType; + private readonly defaultConfig: ConfigType; + private readonly enableFront: boolean; private subscribers: { [key in keyof ConfigType]?: (config: ConfigType) => void } = {}; private allSubscribers: ((config: ConfigType) => void)[] = []; @@ -102,7 +103,7 @@ export class PluginConfig { activePlugins[name] = this; } - async get(key: keyof ConfigType): Promise>> { + get = keyof ConfigType>(key: Key): ConfigType[Key] { return this.config[key]; } @@ -112,11 +113,11 @@ export class PluginConfig { this.#save(); } - getAll() { + getAll(): ConfigType { return { ...this.config }; } - setAll(options: ConfigType) { + setAll(options: Partial>) { if (!options || typeof options !== 'object') { throw new Error('Options must be an object.'); } @@ -124,7 +125,7 @@ export class PluginConfig { let changed = false; for (const [key, value] of Object.entries(options) as Entries) { if (this.config[key] !== value) { - this.config[key] = value; + if (value !== undefined) this.config[key] = value; this.#onChange(key, false); changed = true; } diff --git a/custom-electron-prompt.d.ts b/custom-electron-prompt.d.ts index 9fdceaf0..49e30352 100644 --- a/custom-electron-prompt.d.ts +++ b/custom-electron-prompt.d.ts @@ -24,8 +24,8 @@ declare module 'custom-electron-prompt' { cancel?: string; }; alwaysOnTop?: boolean; - value?: string; - type?: 'input' | 'select' | 'counter'; + value?: unknown; + type?: 'input' | 'select' | 'counter' | 'multiInput'; selectOptions?: Record; keybindOptions?: PromptKeybindOptions[]; counterOptions?: PromptCounterOptions; @@ -37,7 +37,13 @@ declare module 'custom-electron-prompt' { frame?: boolean; customScript?: string; enableRemoteModule?: boolean; - inputAttrs: Partial; + inputAttrs?: Partial; + multiInputOptions?: { + label: string; + value: unknown; + inputAttrs?: Partial; + selectOptions?: Record; + }[]; } const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise; diff --git a/navigation.d.ts b/navigation.d.ts new file mode 100644 index 00000000..7d80de52 --- /dev/null +++ b/navigation.d.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface NavigationOptions { + info: any; +} + +interface NavigationHistoryEntry extends EventTarget { + readonly url?: string; + readonly key: string; + readonly id: string; + readonly index: number; + readonly sameDocument: boolean; + getState(): any; + ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null; +} + +interface NavigationTransition { + readonly navigationType: NavigationType; + readonly from: NavigationHistoryEntry; + readonly finished: Promise; +} + +interface NavigationResult { + committed: Promise; + finished: Promise; +} + +interface NavigationNavigateOptions extends NavigationOptions { + state: any; + history?: NavigationHistoryBehavior; +} + +interface NavigationReloadOptions extends NavigationOptions { + state: any; +} + +interface NavigationUpdateCurrentEntryOptions { + state: any; +} + +interface NavigationEventsMap { + currententrychange: NavigateEvent; + navigate: NavigateEvent; + navigateerror: NavigateEvent; + navigatesuccess: NavigateEvent; +} + +interface Navigation extends EventTarget { + entries(): Array; + readonly currentEntry?: NavigationHistoryEntry; + updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): undefined; + readonly transition?: NavigationTransition; + readonly canGoBack: boolean; + readonly canGoForward: boolean; + navigate(url: string, options?: NavigationNavigateOptions): NavigationResult; + reload(options?: NavigationReloadOptions): NavigationResult; + traverseTo(key: string, options?: NavigationOptions): NavigationResult; + back(options?: NavigationOptions): NavigationResult; + forward(options?: NavigationOptions): NavigationResult; + onnavigate: ((this: Navigation, ev: Event) => any) | null; + onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null; + onnavigateerror: ((this: Navigation, ev: Event) => any) | null; + oncurrententrychange: ((this: Navigation, ev: Event) => any) | null; + + addEventListener(name: K, listener: (event: NavigationEventsMap[K]) => void); +} + +declare class NavigateEvent extends Event { + canIntercept: boolean; + destination: NavigationHistoryEntry; + downloadRequest: string | null; + formData: FormData; + hashChange: boolean; + info: Record; + navigationType: 'push' | 'reload' | 'replace' | 'traverse'; + signal: AbortSignal; + userInitiated: boolean; + + intercept(options?: Record): void; + scroll(): void; +} + +type NavigationHistoryBehavior = 'auto' | 'push' | 'replace'; + +declare const Navigation: { + prototype: Navigation; + new(): Navigation; +}; diff --git a/package-lock.json b/package-lock.json index 0a5d5edd..8bd2a261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "devDependencies": { "@playwright/test": "1.37.1", "@total-typescript/ts-reset": "0.5.1", + "@types/howler": "^2.2.8", "@types/youtube-player": "^5.5.7", "@typescript-eslint/eslint-plugin": "6.5.0", "auto-changelog": "2.4.0", @@ -1200,6 +1201,12 @@ "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.12.tgz", "integrity": "sha512-P20p/YBrqUBmzD6KhIQ8EiY4/RRzlekL4eCvfQnulFPfjmiGxKIoyCeI7qam5I7oKH3P8EU4ptEi0EfyGoLysw==" }, + "node_modules/@types/howler": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.8.tgz", + "integrity": "sha512-7OK+cGHTWIDCOvBlEc61Lzj2tJhCpmeqiqdeNbZvTxLHluBMF6xz/2wjoQkK1M8mJIStp40OdPnkp8xIvwwsuw==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", diff --git a/package.json b/package.json index 5a7dccb6..6fe08c21 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "devDependencies": { "@playwright/test": "1.37.1", "@total-typescript/ts-reset": "0.5.1", + "@types/howler": "^2.2.8", "@types/youtube-player": "^5.5.7", "@typescript-eslint/eslint-plugin": "6.5.0", "auto-changelog": "2.4.0", diff --git a/plugins/adblocker/config.ts b/plugins/adblocker/config.ts index c8c95c45..59cd7ee9 100644 --- a/plugins/adblocker/config.ts +++ b/plugins/adblocker/config.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* renderer */ + import { PluginConfig } from '../../config/dynamic'; const config = new PluginConfig('adblocker', { enableFront: true }); diff --git a/plugins/adblocker/menu.ts b/plugins/adblocker/menu.ts index 2b08aaeb..1e8f43e0 100644 --- a/plugins/adblocker/menu.ts +++ b/plugins/adblocker/menu.ts @@ -1,15 +1,13 @@ -import config from './config'; - -export default async () => { - const blockerConfig = await config.get('blocker'); +import config, { blockers } from './config'; +export default () => { return [ { label: 'Blocker', - submenu: Object.values(config.blockers).map((blocker) => ({ + submenu: Object.values(blockers).map((blocker: string) => ({ label: blocker, type: 'radio', - checked: (blockerConfig || config.blockers.WithBlocklists) === blocker, + checked: (config.get('blocker') || blockers.WithBlocklists) === blocker, click() { config.set('blocker', blocker); }, diff --git a/plugins/audio-compressor/front.js b/plugins/audio-compressor/front.js deleted file mode 100644 index faeb7742..00000000 --- a/plugins/audio-compressor/front.js +++ /dev/null @@ -1,19 +0,0 @@ -const applyCompressor = (e) => { - const { audioContext } = e.detail; - - const compressor = audioContext.createDynamicsCompressor(); - compressor.threshold.value = -50; - compressor.ratio.value = 12; - compressor.knee.value = 40; - compressor.attack.value = 0; - compressor.release.value = 0.25; - - e.detail.audioSource.connect(compressor); - compressor.connect(audioContext.destination); -}; - -module.exports = () => - document.addEventListener('audioCanPlay', applyCompressor, { - once: true, // Only create the audio compressor once, not on each video - passive: true, - }); diff --git a/plugins/audio-compressor/front.ts b/plugins/audio-compressor/front.ts new file mode 100644 index 00000000..66fd94dd --- /dev/null +++ b/plugins/audio-compressor/front.ts @@ -0,0 +1,17 @@ +export default () => + document.addEventListener('audioCanPlay', (e) => { + const { audioContext } = e.detail; + + const compressor = audioContext.createDynamicsCompressor(); + compressor.threshold.value = -50; + compressor.ratio.value = 12; + compressor.knee.value = 40; + compressor.attack.value = 0; + compressor.release.value = 0.25; + + e.detail.audioSource.connect(compressor); + compressor.connect(audioContext.destination); + }, { + once: true, // Only create the audio compressor once, not on each video + passive: true, + }); diff --git a/plugins/blur-nav-bar/back.js b/plugins/blur-nav-bar/back.js deleted file mode 100644 index 897e8bc3..00000000 --- a/plugins/blur-nav-bar/back.js +++ /dev/null @@ -1,7 +0,0 @@ -const path = require('node:path'); - -const { injectCSS } = require('../utils'); - -module.exports = (win) => { - injectCSS(win.webContents, path.join(__dirname, 'style.css')); -}; diff --git a/plugins/blur-nav-bar/back.ts b/plugins/blur-nav-bar/back.ts new file mode 100644 index 00000000..5634e5ef --- /dev/null +++ b/plugins/blur-nav-bar/back.ts @@ -0,0 +1,9 @@ +import path from 'node:path'; + +import { BrowserWindow } from 'electron'; + +import { injectCSS } from '../utils'; + +export default (win: BrowserWindow) => { + injectCSS(win.webContents, path.join(__dirname, 'style.css')); +}; diff --git a/plugins/bypass-age-restrictions/front.js b/plugins/bypass-age-restrictions/front.ts similarity index 88% rename from plugins/bypass-age-restrictions/front.js rename to plugins/bypass-age-restrictions/front.ts index 3c7d9242..21dcab69 100644 --- a/plugins/bypass-age-restrictions/front.js +++ b/plugins/bypass-age-restrictions/front.ts @@ -1,4 +1,4 @@ -module.exports = () => { +export default () => { // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript require('simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js'); }; diff --git a/plugins/captions-selector/back.js b/plugins/captions-selector/back.js deleted file mode 100644 index 2f204fce..00000000 --- a/plugins/captions-selector/back.js +++ /dev/null @@ -1,19 +0,0 @@ -const { ipcMain } = require('electron'); -const prompt = require('custom-electron-prompt'); - -const promptOptions = require('../../providers/prompt-options'); - -module.exports = (win) => { - ipcMain.handle('captionsSelector', async (_, captionLabels, currentIndex) => await prompt( - { - title: 'Choose Caption', - label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`, - type: 'select', - value: currentIndex, - selectOptions: captionLabels, - resizable: true, - ...promptOptions(), - }, - win, - )); -}; diff --git a/plugins/captions-selector/back.ts b/plugins/captions-selector/back.ts new file mode 100644 index 00000000..8073ddde --- /dev/null +++ b/plugins/captions-selector/back.ts @@ -0,0 +1,19 @@ +import { BrowserWindow, ipcMain } from 'electron'; +import prompt from 'custom-electron-prompt'; + +import promptOptions from '../../providers/prompt-options'; + +export default (win: BrowserWindow) => { + ipcMain.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(), + }, + win, + )); +}; diff --git a/plugins/captions-selector/config.js b/plugins/captions-selector/config.js deleted file mode 100644 index 46789327..00000000 --- a/plugins/captions-selector/config.js +++ /dev/null @@ -1,4 +0,0 @@ -const { PluginConfig } = require('../../config/dynamic'); - -const config = new PluginConfig('captions-selector', { enableFront: true }); -module.exports = { ...config }; diff --git a/plugins/captions-selector/config.ts b/plugins/captions-selector/config.ts new file mode 100644 index 00000000..97867c7e --- /dev/null +++ b/plugins/captions-selector/config.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic'; + +const config = new PluginConfig('captions-selector', { enableFront: true }); +export default { ...config } as PluginConfig<'captions-selector'>; diff --git a/plugins/captions-selector/front.js b/plugins/captions-selector/front.ts similarity index 58% rename from plugins/captions-selector/front.js rename to plugins/captions-selector/front.ts index 2eff05ba..cfa61c1b 100644 --- a/plugins/captions-selector/front.js +++ b/plugins/captions-selector/front.ts @@ -1,20 +1,37 @@ -const { ipcRenderer } = require('electron'); +/* eslint-disable @typescript-eslint/await-thenable */ +/* renderer */ -const configProvider = require('./config'); +import { ipcRenderer } from 'electron'; -const { ElementFromFile, templatePath } = require('../utils'); +import configProvider from './config'; -let config; +import { ElementFromFile, templatePath } from '../utils'; +import { YoutubePlayer } from '../../types/youtube-player'; +import { ConfigType } from '../../config/dynamic'; -function $(selector) { - return document.querySelector(selector); +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; } +let config: ConfigType<'captions-selector'>; + +const $ = (selector: string): Element => document.querySelector(selector)!; + const captionsSettingsButton = ElementFromFile( templatePath(__dirname, 'captions-settings-template.html'), ); -module.exports = async () => { +export default async () => { + // RENDERER config = await configProvider.getAll(); configProvider.subscribeAll((newConfig) => { @@ -23,12 +40,12 @@ module.exports = async () => { document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true }); }; -function setup(api) { +function setup(api: YoutubePlayer) { $('.right-controls-buttons').append(captionsSettingsButton); - let captionTrackList = api.getOption('captions', 'tracklist'); + let captionTrackList = api.getOption('captions', 'tracklist') ?? []; - $('video').addEventListener('srcChanged', async () => { + $('video').addEventListener('srcChanged', () => { if (config.disableCaptions) { setTimeout(() => api.unloadModule('captions'), 100); captionsSettingsButton.style.display = 'none'; @@ -37,8 +54,8 @@ function setup(api) { api.loadModule('captions'); - setTimeout(async () => { - captionTrackList = api.getOption('captions', 'tracklist'); + setTimeout(() => { + captionTrackList = api.getOption('captions', 'tracklist') ?? []; if (config.autoload && config.lastCaptionsCode) { api.setOption('captions', 'track', { @@ -54,9 +71,9 @@ function setup(api) { captionsSettingsButton.addEventListener('click', async () => { if (captionTrackList?.length) { - const currentCaptionTrack = api.getOption('captions', 'track'); + const currentCaptionTrack = api.getOption('captions', 'track')!; let currentIndex = currentCaptionTrack - ? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode)) + ? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode)!) : null; const captionLabels = [ @@ -64,7 +81,7 @@ function setup(api) { 'None', ]; - currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex); + currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number; if (currentIndex === null) { return; } diff --git a/plugins/captions-selector/menu.js b/plugins/captions-selector/menu.ts similarity index 77% rename from plugins/captions-selector/menu.js rename to plugins/captions-selector/menu.ts index 42dc202c..e5880cea 100644 --- a/plugins/captions-selector/menu.js +++ b/plugins/captions-selector/menu.ts @@ -1,6 +1,8 @@ -const config = require('./config'); +import config from './config'; -module.exports = () => [ +import { MenuTemplate } from '../../menu'; + +export default (): MenuTemplate => [ { label: 'Automatically select last used caption', type: 'checkbox', diff --git a/plugins/compact-sidebar/front.js b/plugins/compact-sidebar/front.ts similarity index 71% rename from plugins/compact-sidebar/front.js rename to plugins/compact-sidebar/front.ts index 6b2e2b94..b2bd8f03 100644 --- a/plugins/compact-sidebar/front.js +++ b/plugins/compact-sidebar/front.ts @@ -1,10 +1,10 @@ -module.exports = () => { +export default () => { const compactSidebar = document.querySelector('#mini-guide'); const isCompactSidebarDisabled = compactSidebar === null || window.getComputedStyle(compactSidebar).display === 'none'; if (isCompactSidebarDisabled) { - document.querySelector('#button').click(); + (document.querySelector('#button') as HTMLButtonElement)?.click(); } }; diff --git a/plugins/crossfade/back.js b/plugins/crossfade/back.js deleted file mode 100644 index 18d10f86..00000000 --- a/plugins/crossfade/back.js +++ /dev/null @@ -1,15 +0,0 @@ -const { ipcMain } = require('electron'); -const { Innertube } = require('youtubei.js'); - -require('./config'); - -module.exports = async () => { - const yt = await Innertube.create(); - - ipcMain.handle('audio-url', async (_, videoID) => { - const info = await yt.getBasicInfo(videoID); - const url = info.streaming_data?.formats[0].decipher(yt.session.player); - - return url; - }); -}; diff --git a/plugins/crossfade/back.ts b/plugins/crossfade/back.ts new file mode 100644 index 00000000..ff4d1647 --- /dev/null +++ b/plugins/crossfade/back.ts @@ -0,0 +1,15 @@ +import { ipcMain } from 'electron'; +import { Innertube } from 'youtubei.js'; + +import config from './config'; + +export default async () => { + const yt = await Innertube.create(); + + ipcMain.handle('audio-url', async (_, videoID: string) => { + const info = await yt.getBasicInfo(videoID); + const url = info.streaming_data?.formats[0].decipher(yt.session.player); + + return url; + }); +}; diff --git a/plugins/crossfade/config.js b/plugins/crossfade/config.js deleted file mode 100644 index bfd742d7..00000000 --- a/plugins/crossfade/config.js +++ /dev/null @@ -1,4 +0,0 @@ -const { PluginConfig } = require('../../config/dynamic'); - -const config = new PluginConfig('crossfade', { enableFront: true }); -module.exports = { ...config }; diff --git a/plugins/crossfade/config.ts b/plugins/crossfade/config.ts new file mode 100644 index 00000000..186ff14d --- /dev/null +++ b/plugins/crossfade/config.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic'; + +const config = new PluginConfig('crossfade', { enableFront: true }); +export default { ...config } as PluginConfig<'crossfade'>; diff --git a/plugins/crossfade/fader.js b/plugins/crossfade/fader.ts similarity index 83% rename from plugins/crossfade/fader.js rename to plugins/crossfade/fader.ts index df3c8db2..e403100a 100644 --- a/plugins/crossfade/fader.js +++ b/plugins/crossfade/fader.ts @@ -18,7 +18,7 @@ 'use strict'; // Internal utility: check if value is a valid volume level and throw if not -const validateVolumeLevel = (value) => { +const validateVolumeLevel = (value: number) => { // Number between 0 and 1? if (!Number.isNaN(value) && value >= 0 && value <= 1) { // Yup, that's fine @@ -29,8 +29,51 @@ const validateVolumeLevel = (value) => { } }; +type VolumeLogger = (message: string, ...args: Params) => void; +interface VolumeFaderOptions { + /** + * logging `function(stuff, …)` for execution information (default: no logging) + */ + logger?: VolumeLogger; + /** + * either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic) + */ + fadeScaling?: string | number; + /** + * media volume 0…1 to apply during setup (volume not touched by default) + */ + initialVolume?: number; + /** + * time in milliseconds to complete a fade (default: 1000 ms) + */ + fadeDuration?: number; +} + +interface VolumeFade { + volume: { + start: number; + end: number; + }; + time: { + start: number; + end: number; + }; + callback?: () => void; +} + // Main class -class VolumeFader { +export class VolumeFader { + private media: HTMLMediaElement; + private logger: VolumeLogger | false; + private scale: { + internalToVolume: (level: number) => number; + volumeToInternal: (level: number) => number; + }; + private fadeDuration: number = 1000; + private active: boolean = false; + private fade: VolumeFade | undefined; + + /** * VolumeFader Constructor * @@ -38,13 +81,8 @@ class VolumeFader { * @param options {Object} - an object with optional settings * @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid * - * options: - * .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging) - * .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic) - * .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default) - * .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms) */ - constructor(media, options) { + constructor(media: HTMLMediaElement, options: VolumeFaderOptions) { // Passed media element of correct type? if (media instanceof HTMLMediaElement) { // Save reference to media element @@ -70,8 +108,8 @@ class VolumeFader { if (options.fadeScaling === 'linear') { // Pass levels unchanged this.scale = { - internalToVolume: (level) => level, - volumeToInternal: (level) => level, + internalToVolume: (level: number) => level, + volumeToInternal: (level: number) => level, }; // Log setting @@ -79,7 +117,7 @@ class VolumeFader { } // No linear, but logarithmic fading… else { - let dynamicRange; + let dynamicRange: number; // Default dynamic range? if ( @@ -91,7 +129,8 @@ class VolumeFader { } // Custom dynamic range? else if ( - !Number.isNaN(options.fadeScaling) + typeof options.fadeScaling === 'number' + && !Number.isNaN(options.fadeScaling) && options.fadeScaling > 0 ) { // Turn amplitude dB into a multiple of 10 power dB @@ -107,9 +146,9 @@ class VolumeFader { // Use exponential/logarithmic scaler for expansion/compression this.scale = { - internalToVolume: (level) => + internalToVolume: (level: number) => this.exponentialScaler(level, dynamicRange), - volumeToInternal: (level) => + volumeToInternal: (level: number) => this.logarithmicScaler(level, dynamicRange), }; @@ -193,7 +232,7 @@ class VolumeFader { * @throws {TypeError} if fadeDuration is not a number greater than zero * @return {Object} VolumeFader instance for chaining */ - setFadeDuration(fadeDuration) { + setFadeDuration(fadeDuration: number) { // If duration is a valid number > 0… if (!Number.isNaN(fadeDuration) && fadeDuration > 0) { // Set fade duration @@ -219,7 +258,7 @@ class VolumeFader { * @throws {TypeError} if targetVolume is not in the range 0…1 * @return {Object} VolumeFader instance for chaining */ - fadeTo(targetVolume, callback) { + fadeTo(targetVolume: number, callback?: () => void) { // Validate volume and throw if invalid validateVolumeLevel(targetVolume); @@ -250,11 +289,11 @@ class VolumeFader { } // Convenience shorthand methods for common fades - fadeIn(callback) { + fadeIn(callback: () => void) { this.fadeTo(1, callback); } - fadeOut(callback) { + fadeOut(callback: () => void) { this.fadeTo(0, callback); } @@ -313,7 +352,7 @@ class VolumeFader { * @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞) * @return {Number} - expanded level (float, 0…1) */ - exponentialScaler(input, dynamicRange) { + exponentialScaler(input: number, dynamicRange: number) { // Special case: make zero (or any falsy input) return zero if (input === 0) { // Since the dynamic range is limited, @@ -336,7 +375,7 @@ class VolumeFader { * @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞) * @return {Number} - compressed level (float, 0…1) */ - logarithmicScaler(input, dynamicRange) { + logarithmicScaler(input: number, dynamicRange: number) { // Special case: make zero (or any falsy input) return zero if (input === 0) { // Logarithm of zero would be -∞, which would map to zero anyway @@ -351,6 +390,6 @@ class VolumeFader { } } -module.exports = { +export default { VolumeFader }; diff --git a/plugins/crossfade/front.js b/plugins/crossfade/front.ts similarity index 59% rename from plugins/crossfade/front.js rename to plugins/crossfade/front.ts index 526623f7..aeff1064 100644 --- a/plugins/crossfade/front.js +++ b/plugins/crossfade/front.ts @@ -1,38 +1,39 @@ -const { ipcRenderer } = require('electron'); -const { Howl } = require('howler'); +/* eslint-disable @typescript-eslint/await-thenable */ +/* renderer */ + +import { ipcRenderer } from 'electron'; +import { Howl } from 'howler'; // Extracted from https://github.com/bitfasching/VolumeFader -const { VolumeFader } = require('./fader'); +import { VolumeFader } from './fader'; -let transitionAudio; // Howler audio used to fade out the current music +import configProvider from './config'; + +import defaultConfigs from '../../config/defaults'; +import { ConfigType } from '../../config/dynamic'; + +let transitionAudio: Howl; // Howler audio used to fade out the current music let firstVideo = true; -let waitForTransition; +let waitForTransition: Promise; -/** - * @type {PluginConfig} - */ -const configProvider = require('./config'); +const defaultConfig = defaultConfigs.plugins.crossfade; -const defaultConfig = require('../../config/defaults').plugins.crossfade; +let config: ConfigType<'crossfade'>; -let config; +const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number); -const configGetNumber = (key) => Number(config[key]) || defaultConfig[key]; +const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise; -const getStreamURL = async (videoID) => { - return await ipcRenderer.invoke('audio-url', videoID); -}; - -const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); +const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded'; -const watchVideoIDChanges = (cb) => { +const watchVideoIDChanges = (cb: (id: string) => void) => { window.navigation.addEventListener('navigate', (event) => { const currentVideoID = getVideoIDFromURL( - event.currentTarget.currentEntry.url, + (event.currentTarget as Navigation).currentEntry?.url ?? '', ); - const nextVideoID = getVideoIDFromURL(event.destination.url); + const nextVideoID = getVideoIDFromURL(event.destination.url ?? ''); if ( nextVideoID @@ -51,7 +52,7 @@ const watchVideoIDChanges = (cb) => { }); }; -const createAudioForCrossfade = async (url) => { +const createAudioForCrossfade = (url: string) => { if (transitionAudio) { transitionAudio.unload(); } @@ -61,19 +62,19 @@ const createAudioForCrossfade = async (url) => { html5: true, volume: 0, }); - await syncVideoWithTransitionAudio(); + syncVideoWithTransitionAudio(); }; -const syncVideoWithTransitionAudio = async () => { - const video = document.querySelector('video'); +const syncVideoWithTransitionAudio = () => { + const video = document.querySelector('video')!; const videoFader = new VolumeFader(video, { fadeScaling: configGetNumber('fadeScaling'), fadeDuration: configGetNumber('fadeInDuration'), }); - await transitionAudio.play(); - await transitionAudio.seek(video.currentTime); + transitionAudio.play(); + transitionAudio.seek(video.currentTime); video.addEventListener('seeking', () => { transitionAudio.seek(video.currentTime); @@ -83,9 +84,9 @@ const syncVideoWithTransitionAudio = async () => { transitionAudio.pause(); }); - video.addEventListener('play', async () => { - await transitionAudio.play(); - await transitionAudio.seek(video.currentTime); + video.addEventListener('play', () => { + transitionAudio.play(); + transitionAudio.seek(video.currentTime); // Fade in const videoVolume = video.volume; @@ -102,7 +103,7 @@ const syncVideoWithTransitionAudio = async () => { video.removeEventListener('timeupdate', transitionBeforeEnd); // Go to next video - XXX: does not support "repeat 1" mode - document.querySelector('.next-button').click(); + (document.querySelector('.next-button') as HTMLButtonElement).click(); } }; @@ -121,18 +122,18 @@ const onApiLoaded = () => { }); }; -const crossfade = async (cb) => { +const crossfade = (cb: () => void) => { if (!isReadyToCrossfade()) { cb(); return; } - let resolveTransition; - waitForTransition = new Promise((resolve) => { + let resolveTransition: () => void; + waitForTransition = new Promise((resolve) => { resolveTransition = resolve; }); - const video = document.querySelector('video'); + const video = document.querySelector('video')!; const fader = new VolumeFader(transitionAudio._sounds[0]._node, { initialVolume: video.volume, @@ -148,7 +149,7 @@ const crossfade = async (cb) => { }); }; -module.exports = async () => { +export default async () => { config = await configProvider.getAll(); configProvider.subscribeAll((newConfig) => { diff --git a/plugins/crossfade/menu.js b/plugins/crossfade/menu.ts similarity index 70% rename from plugins/crossfade/menu.js rename to plugins/crossfade/menu.ts index c39c14dc..93034d48 100644 --- a/plugins/crossfade/menu.js +++ b/plugins/crossfade/menu.ts @@ -1,11 +1,16 @@ -const prompt = require('custom-electron-prompt'); +import prompt from 'custom-electron-prompt'; -const config = require('./config'); +import { BrowserWindow } from 'electron'; -const promptOptions = require('../../providers/prompt-options'); -const defaultOptions = require('../../config/defaults').plugins.crossfade; +import config from './config'; -module.exports = (win) => [ +import promptOptions from '../../providers/prompt-options'; +import configOptions from '../../config/defaults'; +import { ConfigType } from '../../config/dynamic'; + +const defaultOptions = configOptions.plugins.crossfade; + +export default (win: BrowserWindow) => [ { label: 'Advanced', async click() { @@ -17,7 +22,7 @@ module.exports = (win) => [ }, ]; -async function promptCrossfadeValues(win, options) { +async function promptCrossfadeValues(win: BrowserWindow, options: ConfigType<'crossfade'>): Promise> | undefined> { const res = await prompt( { title: 'Crossfade Options', @@ -29,8 +34,8 @@ async function promptCrossfadeValues(win, options) { inputAttrs: { type: 'number', required: true, - min: 0, - step: 100, + min: '0', + step: '100', }, }, { @@ -39,8 +44,8 @@ async function promptCrossfadeValues(win, options) { inputAttrs: { type: 'number', required: true, - min: 0, - step: 100, + min: '0', + step: '100', }, }, { @@ -50,7 +55,7 @@ async function promptCrossfadeValues(win, options) { inputAttrs: { type: 'number', required: true, - min: 0, + min: '0', }, }, { diff --git a/plugins/disable-autoplay/front.js b/plugins/disable-autoplay/front.js deleted file mode 100644 index c9cca0d6..00000000 --- a/plugins/disable-autoplay/front.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = () => { - document.addEventListener('apiLoaded', (apiEvent) => { - apiEvent.detail.addEventListener('videodatachange', (name) => { - if (name === 'dataloaded') { - apiEvent.detail.pauseVideo(); - document.querySelector('video').addEventListener('timeupdate', (e) => { - e.target.pause(); - }); - } else { - document.querySelector('video').ontimeupdate = null; - } - }); - }, { once: true, passive: true }); -}; diff --git a/plugins/disable-autoplay/front.ts b/plugins/disable-autoplay/front.ts new file mode 100644 index 00000000..5e4cd47d --- /dev/null +++ b/plugins/disable-autoplay/front.ts @@ -0,0 +1,14 @@ +export default () => { + document.addEventListener('apiLoaded', (apiEvent) => { + apiEvent.detail.addEventListener('videodatachange', (name: string) => { + if (name === 'dataloaded') { + apiEvent.detail.pauseVideo(); + (document.querySelector('video') as HTMLVideoElement)?.addEventListener('timeupdate', (e) => { + (e.target as HTMLVideoElement)?.pause(); + }); + } else { + (document.querySelector('video') as HTMLVideoElement).ontimeupdate = null; + } + }); + }, { once: true, passive: true }); +}; diff --git a/plugins/discord/back.js b/plugins/discord/back.ts similarity index 68% rename from plugins/discord/back.js rename to plugins/discord/back.ts index 145e20a6..eb9ce125 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.ts @@ -1,36 +1,35 @@ -'use strict'; -const { dialog, app } = require('electron'); -const Discord = require('@xhayper/discord-rpc'); -const { dev } = require('electron-is'); +import { app, dialog } from 'electron'; +import Discord from '@xhayper/discord-rpc'; +import { dev } from 'electron-is'; -const registerCallback = require('../../providers/song-info'); +import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser'; + +import registerCallback from '../../providers/song-info'; +import pluginConfig from '../../config'; // Application ID registered by @Zo-Bro-23 const clientId = '1043858434585526382'; -/** - * @typedef {Object} Info - * @property {import('@xhayper/discord-rpc').Client} rpc - * @property {boolean} ready - * @property {boolean} autoReconnect - * @property {import('../../providers/song-info').SongInfo} lastSongInfo - */ -/** - * @type {Info} - */ -const info = { +export interface Info { + rpc: import('@xhayper/discord-rpc').Client; + ready: boolean; + autoReconnect: boolean; + lastSongInfo?: import('../../providers/song-info').SongInfo; +} + +const info: Info = { rpc: new Discord.Client({ clientId, }), ready: false, autoReconnect: true, - lastSongInfo: null, + lastSongInfo: undefined, }; /** * @type {(() => void)[]} */ -const refreshCallbacks = []; +const refreshCallbacks: (() => void)[] = []; const resetInfo = () => { info.ready = false; @@ -85,8 +84,8 @@ const connectRecursive = () => { connectTimeout().catch(connectRecursive); }; -let window; -const connect = (showError = false) => { +let window: Electron.BrowserWindow; +export const connect = (showError = false) => { if (info.rpc.isConnected) { if (dev()) { console.log('Attempted to connect with active connection'); @@ -98,7 +97,7 @@ const connect = (showError = false) => { info.ready = false; // Startup the rpc client - info.rpc.login({ clientId }).catch((error) => { + info.rpc.login().catch((error: Error) => { resetInfo(); if (dev()) { console.error(error); @@ -116,14 +115,17 @@ const connect = (showError = false) => { }); }; -let clearActivity; -/** - * @type {import('../../providers/song-info').songInfoCallback} - */ -let updateActivity; +let clearActivity: NodeJS.Timeout | undefined; +let updateActivity: import('../../providers/song-info').SongInfoCallback; -module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTime, listenAlong, hideDurationLeft }) => { - info.autoReconnect = autoReconnect; +const DiscordOptionsObj = pluginConfig.get('plugins.discord'); +type DiscordOptions = typeof DiscordOptionsObj; + +export default ( + win: Electron.BrowserWindow, + options: DiscordOptions, +) => { + info.autoReconnect = options.autoReconnect; window = win; // We get multiple events @@ -146,7 +148,7 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim } // Clear directly if timeout is 0 - if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { + if (songInfo.isPaused && options.activityTimoutEnabled && options.activityTimoutTime === 0) { info.rpc.user?.clearActivity().catch(console.error); return; } @@ -154,12 +156,12 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim // Song information changed, so lets update the rich presence // @see https://discord.com/developers/docs/topics/gateway#activity-object // not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530 - const activityInfo = { + const activityInfo: SetActivity = { details: songInfo.title, state: songInfo.artist, - largeImageKey: songInfo.imageSrc, - largeImageText: songInfo.album, - buttons: listenAlong ? [ + largeImageKey: songInfo.imageSrc ?? '', + largeImageText: songInfo.album ?? '', + buttons: options.listenAlong ? [ { label: 'Listen Along', url: songInfo.url }, ] : undefined, }; @@ -169,10 +171,10 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim activityInfo.smallImageKey = 'paused'; activityInfo.smallImageText = 'Paused'; // Set start the timer so the activity gets cleared after a while if enabled - if (activityTimoutEnabled) { - clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10_000); + if (options.activityTimoutEnabled) { + clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), options.activityTimoutTime ?? 10_000); } - } else if (!hideDurationLeft) { + } else if (!options.hideDurationLeft) { // Add the start and end time of the song const songStartTime = Date.now() - (songInfo.elapsedSeconds * 1000); activityInfo.startTimestamp = songStartTime; @@ -188,10 +190,10 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim registerCallback(updateActivity); connect(); }); - app.on('window-all-closed', module.exports.clear); + app.on('window-all-closed', clear); }; -module.exports.clear = () => { +export const clear = () => { if (info.rpc) { info.rpc.user?.clearActivity(); } @@ -199,6 +201,5 @@ module.exports.clear = () => { clearTimeout(clearActivity); }; -module.exports.connect = connect; -module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb); -module.exports.isConnected = () => info.rpc !== null; +export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb); +export const isConnected = () => info.rpc !== null; diff --git a/plugins/discord/menu.js b/plugins/discord/menu.ts similarity index 60% rename from plugins/discord/menu.js rename to plugins/discord/menu.ts index c2086586..6723b3f6 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.ts @@ -1,16 +1,22 @@ -const prompt = require('custom-electron-prompt'); +import prompt from 'custom-electron-prompt'; -const { clear, connect, registerRefresh, isConnected } = require('./back'); +import { Electron } from 'playwright'; -const { setMenuOptions } = require('../../config/plugins'); -const promptOptions = require('../../providers/prompt-options'); -const { singleton } = require('../../providers/decorators'); +import { clear, connect, isConnected, registerRefresh } from './back'; -const registerRefreshOnce = singleton((refreshMenu) => { +import { setMenuOptions } from '../../config/plugins'; +import promptOptions from '../../providers/prompt-options'; +import { singleton } from '../../providers/decorators'; +import config from '../../config'; + +const registerRefreshOnce = singleton((refreshMenu: () => void) => { registerRefresh(refreshMenu); }); -module.exports = (win, options, refreshMenu) => { +const DiscordOptionsObj = config.get('plugins.discord'); +type DiscordOptions = typeof DiscordOptionsObj; + +export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => { registerRefreshOnce(refreshMenu); return [ @@ -23,7 +29,7 @@ module.exports = (win, options, refreshMenu) => { label: 'Auto reconnect', type: 'checkbox', checked: options.autoReconnect, - click(item) { + click(item: Electron.MenuItem) { options.autoReconnect = item.checked; setMenuOptions('discord', options); }, @@ -36,7 +42,7 @@ module.exports = (win, options, refreshMenu) => { label: 'Clear activity after timeout', type: 'checkbox', checked: options.activityTimoutEnabled, - click(item) { + click(item: Electron.MenuItem) { options.activityTimoutEnabled = item.checked; setMenuOptions('discord', options); }, @@ -45,7 +51,7 @@ module.exports = (win, options, refreshMenu) => { label: 'Listen Along', type: 'checkbox', checked: options.listenAlong, - click(item) { + click(item: Electron.MenuItem) { options.listenAlong = item.checked; setMenuOptions('discord', options); }, @@ -54,7 +60,7 @@ module.exports = (win, options, refreshMenu) => { label: 'Hide duration left', type: 'checkbox', checked: options.hideDurationLeft, - click(item) { + click(item: Electron.MenuItem) { options.hideDurationLeft = item.checked; setMenuOptions('discord', options); }, @@ -66,11 +72,11 @@ module.exports = (win, options, refreshMenu) => { ]; }; -async function setInactivityTimeout(win, options) { +async function setInactivityTimeout(win: Electron.BrowserWindow, options: DiscordOptions) { const output = await prompt({ title: 'Set Inactivity Timeout', label: 'Enter inactivity timeout in seconds:', - value: Math.round((options.activityTimoutTime ?? 0) / 1e3), + value: String(Math.round((options.activityTimoutTime ?? 0) / 1e3)), type: 'counter', counterOptions: { minimum: 0, multiFire: true }, width: 450, @@ -78,7 +84,7 @@ async function setInactivityTimeout(win, options) { }, win); if (output) { - options.activityTimoutTime = Math.round(output * 1e3); + options.activityTimoutTime = Math.round(~~output * 1e3); setMenuOptions('discord', options); } } diff --git a/plugins/skip-silences/front.js b/plugins/skip-silences/front.ts similarity index 77% rename from plugins/skip-silences/front.js rename to plugins/skip-silences/front.ts index bf2508fb..6ad8853a 100644 --- a/plugins/skip-silences/front.js +++ b/plugins/skip-silences/front.ts @@ -1,4 +1,9 @@ -module.exports = (options) => { +import config from '../../config'; + +const SkipSilencesOptionsObj = config.get('plugins.skip-silences'); +type SkipSilencesOptions = typeof SkipSilencesOptionsObj; + +export default (options: SkipSilencesOptions) => { let isSilent = false; let hasAudioStarted = false; @@ -6,7 +11,7 @@ module.exports = (options) => { const threshold = -100; // DB (-100 = absolute silence, 0 = loudest) const interval = 2; // Ms const history = 10; - const speakingHistory = Array.from({ length: history }).fill(0); + const speakingHistory = Array.from({ length: history }).fill(0) as number[]; document.addEventListener( 'audioCanPlay', @@ -53,11 +58,13 @@ module.exports = (options) => { if (history == 0 // Silent && !( - video.paused - || video.seeking - || video.ended - || video.muted - || video.volume === 0 + video && ( + video.paused + || video.seeking + || video.ended + || video.muted + || video.volume === 0 + ) ) ) { isSilent = true; @@ -66,7 +73,7 @@ module.exports = (options) => { } speakingHistory.shift(); - speakingHistory.push(0 + (currentVolume > threshold)); + speakingHistory.push(Number(currentVolume > threshold)); looper(); }, interval); @@ -79,17 +86,17 @@ module.exports = (options) => { return; } - if (isSilent && !video.paused) { + if (isSilent && video && !video.paused) { video.currentTime += 0.2; // In s } }; - video.addEventListener('play', () => { + video?.addEventListener('play', () => { hasAudioStarted = false; skipSilence(); }); - video.addEventListener('seeked', () => { + video?.addEventListener('seeked', () => { hasAudioStarted = false; skipSilence(); }); @@ -100,7 +107,7 @@ module.exports = (options) => { ); }; -function getMaxVolume(analyser, fftBins) { +function getMaxVolume(analyser: AnalyserNode, fftBins: Float32Array) { let maxVolume = Number.NEGATIVE_INFINITY; analyser.getFloatFrequencyData(fftBins); diff --git a/plugins/taskbar-mediacontrol/back.js b/plugins/taskbar-mediacontrol/back.ts similarity index 54% rename from plugins/taskbar-mediacontrol/back.js rename to plugins/taskbar-mediacontrol/back.ts index 92d11fa5..ea411b23 100644 --- a/plugins/taskbar-mediacontrol/back.js +++ b/plugins/taskbar-mediacontrol/back.ts @@ -1,12 +1,19 @@ -const path = require('node:path'); +import path from 'node:path'; -const getSongControls = require('../../providers/song-controls'); -const registerCallback = require('../../providers/song-info'); +import { BrowserWindow, nativeImage } from 'electron'; -let controls; -let currentSongInfo; +import getSongControls from '../../providers/song-controls'; +import registerCallback, { SongInfo } from '../../providers/song-info'; -module.exports = (win) => { + +let controls: { + playPause: () => void; + next: () => void; + previous: () => void; +}; +let currentSongInfo: SongInfo; + +export default (win: BrowserWindow) => { const { playPause, next, previous } = getSongControls(win); controls = { playPause, next, previous }; @@ -23,7 +30,7 @@ module.exports = (win) => { }); }; -function setThumbar(win, songInfo) { +function setThumbar(win: BrowserWindow, songInfo: SongInfo) { // Wait for song to start before setting thumbar if (!songInfo?.title) { return; @@ -33,28 +40,28 @@ function setThumbar(win, songInfo) { win.setThumbarButtons([ { tooltip: 'Previous', - icon: get('previous'), + icon: nativeImage.createFromPath(get('previous')), click() { - controls.previous(win.webContents); + controls.previous(); }, }, { tooltip: 'Play/Pause', // Update icon based on play state - icon: songInfo.isPaused ? get('play') : get('pause'), + icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')), click() { - controls.playPause(win.webContents); + controls.playPause(); }, }, { tooltip: 'Next', - icon: get('next'), + icon: nativeImage.createFromPath(get('next')), click() { - controls.next(win.webContents); + controls.next(); }, }, ]); } // Util -function get(kind) { +function get(kind: string) { return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`); } diff --git a/plugins/touchbar/back.js b/plugins/touchbar/back.ts similarity index 82% rename from plugins/touchbar/back.js rename to plugins/touchbar/back.ts index 8696a6b9..01091b33 100644 --- a/plugins/touchbar/back.js +++ b/plugins/touchbar/back.ts @@ -1,4 +1,7 @@ -const { TouchBar } = require('electron'); +import { TouchBar, NativeImage, BrowserWindow } from 'electron'; + +import registerCallback from '../../providers/song-info'; +import getSongControls from '../../providers/song-controls'; const { TouchBarButton, @@ -8,21 +11,20 @@ const { TouchBarScrubber, } = TouchBar; -const registerCallback = require('../../providers/song-info'); -const getSongControls = require('../../providers/song-controls'); - // Songtitle label const songTitle = new TouchBarLabel({ label: '', }); // This will store the song controls once available -let controls = []; +let controls: (() => void)[] = []; // This will store the song image once available -const songImage = {}; +const songImage: { + icon?: NativeImage; +} = {}; // Pause/play button -const pausePlayButton = new TouchBarButton(); +const pausePlayButton = new TouchBarButton({}); // The song control buttons (control functions are in the same order) const buttons = new TouchBarSegmentedControl({ @@ -59,7 +61,7 @@ const touchBar = new TouchBar({ ], }); -module.exports = (win) => { +module.exports = (win: BrowserWindow) => { const { playPause, next, previous, dislike, like } = getSongControls(win); // If the page is ready, register the callback @@ -79,7 +81,7 @@ module.exports = (win) => { // Get image source songImage.icon = songInfo.image ? songInfo.image.resize({ height: 23 }) - : null; + : undefined; win.setTouchBar(touchBar); }); diff --git a/plugins/tuna-obs/back.js b/plugins/tuna-obs/back.ts similarity index 55% rename from plugins/tuna-obs/back.js rename to plugins/tuna-obs/back.ts index 96e83381..8df6fd98 100644 --- a/plugins/tuna-obs/back.js +++ b/plugins/tuna-obs/back.ts @@ -1,13 +1,26 @@ -const { ipcMain, net } = require('electron'); +import { ipcMain, net, BrowserWindow } from 'electron'; -const registerCallback = require('../../providers/song-info'); +import registerCallback from '../../providers/song-info'; -const secToMilisec = (t) => Math.round(Number(t) * 1e3); -const data = { +const secToMilisec = (t: number) => Math.round(Number(t) * 1e3); + +interface Data { + album: string | null | undefined; + album_url: string; + artists: string[]; + cover: string; + cover_url: string; + duration: number; + progress: number; + status: string; + title: string; +} + +const data: Data = { cover: '', cover_url: '', title: '', - artists: [], + artists: [] as string[], status: '', progress: 0, duration: 0, @@ -15,7 +28,7 @@ const data = { album: undefined, }; -const post = async (data) => { +const post = (data: Data) => { const port = 1608; const headers = { 'Content-Type': 'application/json', @@ -28,13 +41,12 @@ const post = async (data) => { method: 'POST', headers, body: JSON.stringify({ data }), - }).catch((error) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`)); + }).catch((error: { code: number, errno: number }) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`)); }; -/** @param {Electron.BrowserWindow} win */ -module.exports = async (win) => { +module.exports = (win: BrowserWindow) => { ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener')); - ipcMain.on('timeChanged', async (_, t) => { + ipcMain.on('timeChanged', (_, t: number) => { if (!data.title) { return; } @@ -50,9 +62,9 @@ module.exports = async (win) => { data.duration = secToMilisec(songInfo.songDuration); data.progress = secToMilisec(songInfo.elapsedSeconds); - data.cover = songInfo.imageSrc; - data.cover_url = songInfo.imageSrc; - data.album_url = songInfo.imageSrc; + data.cover = songInfo.imageSrc ?? ''; + data.cover_url = songInfo.imageSrc ?? ''; + data.album_url = songInfo.imageSrc ?? ''; data.title = songInfo.title; data.artists = [songInfo.artist]; data.status = songInfo.isPaused ? 'stopped' : 'playing'; diff --git a/plugins/utils.ts b/plugins/utils.ts index 431b08b1..e5bfdc63 100644 --- a/plugins/utils.ts +++ b/plugins/utils.ts @@ -7,11 +7,12 @@ import { ValueOf } from '../utils/type-utils'; // Creates a DOM element from an HTML string -export const ElementFromHtml = (html: string) => { +export const ElementFromHtml = (html: string): HTMLElement => { const template = document.createElement('template'); html = html.trim(); // Never return a text node of whitespace as the result template.innerHTML = html; - return template.content.firstChild; + + return template.content.firstElementChild as HTMLElement; }; // Creates a DOM element from a HTML file diff --git a/providers/song-info.ts b/providers/song-info.ts index 655a5336..486daef9 100644 --- a/providers/song-info.ts +++ b/providers/song-info.ts @@ -88,7 +88,7 @@ const handleData = async (responseText: string, win: Electron.BrowserWindow) => }; // This variable will be filled with the callbacks once they register -type SongInfoCallback = (songInfo: SongInfo, event: string) => void; +export type SongInfoCallback = (songInfo: SongInfo, event?: string) => void; const callbacks: SongInfoCallback[] = []; // This function will allow plugins to register callback that will be triggered when data changes diff --git a/reset.d.ts b/reset.d.ts index 31759405..b3b3d290 100644 --- a/reset.d.ts +++ b/reset.d.ts @@ -2,8 +2,14 @@ import '@total-typescript/ts-reset'; import { YoutubePlayer } from './types/youtube-player'; declare global { + interface Compressor { + audioSource: MediaElementAudioSourceNode; + audioContext: AudioContext; + } + interface DocumentEventMap { 'apiLoaded': CustomEvent; + 'audioCanPlay': CustomEvent; } interface Window { @@ -11,5 +17,18 @@ declare global { * YouTube Music internal variable (Last interaction time) */ _lact: number; + navigation: Navigation; + } +} + + // import { Howl as _Howl } from 'howler'; +declare module 'howler' { + interface Howl { + _sounds: { + _paused: boolean; + _ended: boolean; + _id: string; + _node: HTMLMediaElement; + }[]; } } diff --git a/tsconfig.json b/tsconfig.json index 3d764405..d4d53dcd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "ESNext", + "lib": ["dom"], "module": "CommonJS", "allowSyntheticDefaultImports": true, "esModuleInterop": true, diff --git a/types/youtube-player.ts b/types/youtube-player.ts index 171c9a49..51ee4c1f 100644 --- a/types/youtube-player.ts +++ b/types/youtube-player.ts @@ -140,9 +140,9 @@ export interface YoutubePlayer { getPlaylistId: (...params: Parameters) => Return; loadModule: (...params: Parameters) => Return; unloadModule: (...params: Parameters) => Return; - setOption: (...params: Parameters) => Return; - getOption: (...params: Parameters) => Return; - getOptions: (...params: Parameters) => Return; + setOption: (optionName: string, key: string, value: T) => void; + getOption: (optionName: string, key: string) => T | null | undefined; + getOptions: () => string[]; mute: (...params: Parameters) => Return; unMute: (...params: Parameters) => Return; isMuted: (...params: Parameters) => Return;