diff --git a/config/defaults.ts b/config/defaults.ts index a09e916f..f5143f32 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -83,6 +83,16 @@ const defaultConfig = { 'shortcuts': { enabled: false, overrideMediaKeys: false, + global: { + previous: '', + playPause: '', + next: '', + } as Record, + local: { + previous: '', + playPause: '', + next: '', + } as Record, }, 'downloader': { enabled: false, @@ -130,7 +140,7 @@ const defaultConfig = { volumeUp: '', volumeDown: '', }, - savedVolume: undefined, // Plugin save volume between session here + savedVolume: undefined as number | undefined, // Plugin save volume between session here }, 'sponsorblock': { enabled: false, @@ -146,8 +156,10 @@ const defaultConfig = { }, 'video-toggle': { enabled: false, + hideVideo: false, mode: 'custom', forceHide: false, + align: '', }, 'picture-in-picture': { 'enabled': false, @@ -158,6 +170,7 @@ const defaultConfig = { 'pip-position': [10, 10], 'pip-size': [450, 275], 'isInPiP': false, + 'useNativePiP': false, }, 'captions-selector': { enabled: false, diff --git a/custom-electron-prompt.d.ts b/custom-electron-prompt.d.ts index 49e30352..6f08d3d1 100644 --- a/custom-electron-prompt.d.ts +++ b/custom-electron-prompt.d.ts @@ -1,19 +1,29 @@ declare module 'custom-electron-prompt' { import { BrowserWindow } from 'electron'; - export interface PromptCounterOptions { + export type SelectOptions = Record; + + export interface CounterOptions { minimum?: number; maximum?: number; multiFire?: boolean; } - export interface PromptKeybindOptions { + export interface KeybindOptions { value: string; label: string; - default: string; + default?: string; } - export interface PromptOptions { + export interface InputOptions { + label: string; + value: unknown; + inputAttrs?: Partial; + selectOptions?: SelectOptions; + } + + interface BasePromptOptions { + type?: T; width?: number; height?: number; resizable?: boolean; @@ -25,10 +35,6 @@ declare module 'custom-electron-prompt' { }; alwaysOnTop?: boolean; value?: unknown; - type?: 'input' | 'select' | 'counter' | 'multiInput'; - selectOptions?: Record; - keybindOptions?: PromptKeybindOptions[]; - counterOptions?: PromptCounterOptions; icon?: string; useHtmlLabel?: boolean; customStylesheet?: string; @@ -38,15 +44,42 @@ declare module 'custom-electron-prompt' { customScript?: string; enableRemoteModule?: boolean; inputAttrs?: Partial; - multiInputOptions?: { - label: string; - value: unknown; - inputAttrs?: Partial; - selectOptions?: Record; - }[]; } - const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise; + export type InputPromptOptions = BasePromptOptions<'input'>; + export interface SelectPromptOptions extends BasePromptOptions<'select'> { + selectOptions: SelectOptions; + } + export interface CounterPromptOptions extends BasePromptOptions<'counter'> { + counterOptions: CounterOptions; + } + export interface MultiInputPromptOptions extends BasePromptOptions<'multiInput'> { + multiInputOptions: InputOptions[]; + } + export interface KeybindPromptOptions extends BasePromptOptions<'keybind'> { + keybindOptions: KeybindOptions[]; + } + + export type PromptOptions = ( + T extends 'input' ? InputPromptOptions : + T extends 'select' ? SelectPromptOptions : + T extends 'counter' ? CounterPromptOptions : + T extends 'keybind' ? KeybindPromptOptions : + T extends 'multiInput' ? MultiInputPromptOptions : + never + ); + + type PromptResult = T extends 'input' ? string : + T extends 'select' ? string : + T extends 'counter' ? number : + T extends 'keybind' ? { + value: string; + accelerator: string + }[] : + T extends 'multiInput' ? string[] : + never; + + const prompt: (options?: PromptOptions & { type: T }, parent?: BrowserWindow) => Promise | null>; export default prompt; } diff --git a/menu.ts b/menu.ts index 629d7299..0377bb5d 100644 --- a/menu.ts +++ b/menu.ts @@ -58,8 +58,9 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => { } type PluginType = (window: BrowserWindow, plugins: string, func: () => void) => Electron.MenuItemConstructorOptions[]; - // eslint-disable-next-line @typescript-eslint/no-var-requires - const getPluginMenu = require(pluginPath) as PluginType; + + // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access + const getPluginMenu = require(pluginPath).default as PluginType; return { label: pluginLabel, submenu: [ @@ -274,7 +275,7 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => { { label: 'Proxy', type: 'checkbox', - checked: Boolean(config.get('options.proxy')), + checked: !!(config.get('options.proxy')), click(item) { setProxy(item, win); }, diff --git a/package-lock.json b/package-lock.json index 51f51d3d..b804d614 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "@types/youtube-player": "^5.5.7", "@typescript-eslint/eslint-plugin": "6.5.0", "auto-changelog": "2.4.0", + "copyfiles": "2.4.1", "del-cli": "5.0.1", "electron": "27.0.0-alpha.5", "electron-builder": "24.6.3", @@ -2834,6 +2835,76 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -3162,6 +3233,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3543,6 +3629,21 @@ "unzip-crx-3": "^0.2.0" } }, + "node_modules/electron-devtools-installer/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/electron-is": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/electron-is/-/electron-is-3.0.0.tgz", @@ -4507,6 +4608,21 @@ "node": ">=12.0.0" } }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", @@ -6596,6 +6712,21 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-id3": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz", @@ -6615,6 +6746,40 @@ "node": ">=0.10.0" } }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -7617,21 +7782,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -8476,6 +8626,52 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -8522,6 +8718,21 @@ "tmp": "^0.2.0" } }, + "node_modules/tmp-promise/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tmp-promise/node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -9079,6 +9290,15 @@ "node": ">=8.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3c2c0ee3..d1c01766 100644 --- a/package.json +++ b/package.json @@ -84,21 +84,22 @@ "scripts": { "test": "playwright test", "test:debug": "DEBUG=pw:browser* playwright test", - "start": "tsc && electron ./dist/index.js", + "start": "tsc && npm run copy-files && electron ./dist/index.js", "start:debug": "ELECTRON_ENABLE_LOGGING=1 electron ./dist/index.js", "generate:package": "node utils/generate-package-json.js", "postinstall": "npm run plugins", "clean": "del-cli dist && del-cli pack", - "build": "npm run clean && tsc && electron-builder --win --mac --linux -p never", - "build:linux": "npm run clean && electron-builder --linux -p never", - "build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never", - "build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never", - "build:win": "npm run clean && electron-builder --win -p never", - "build:win:x64": "npm run clean && electron-builder --win nsis:x64 -p never", + "copy-files": "copyfiles -u 1 plugins/**/*.html plugins/**/*.css plugins/**/*.bin plugins/**/*.js dist/plugins/", + "build": "npm run clean && tsc && npm run copy-files && electron-builder --win --mac --linux -p never", + "build:linux": "npm run clean && tsc && npm run copy-files && electron-builder --linux -p never", + "build:mac": "npm run clean && tsc && npm run copy-files && electron-builder --mac dmg:x64 -p never", + "build:mac:arm64": "npm run clean && tsc && npm run copy-files && electron-builder --mac dmg:arm64 -p never", + "build:win": "npm run clean && tsc && npm run copy-files && electron-builder --win -p never", + "build:win:x64": "npm run clean && tsc && npm run copy-files && electron-builder --win nsis:x64 -p never", "lint": "xo", "changelog": "auto-changelog", "plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions", - "plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && tsc plugins/adblocker/blocker.ts && node dist/plugins/adblocker/blocker.js", + "plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && tsc && node dist/plugins/adblocker/blocker.js", "plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass", "release:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github", "release:mac": "npm run clean && electron-builder --mac -p always", @@ -157,6 +158,7 @@ "@types/youtube-player": "^5.5.7", "@typescript-eslint/eslint-plugin": "6.5.0", "auto-changelog": "2.4.0", + "copyfiles": "2.4.1", "del-cli": "5.0.1", "electron": "27.0.0-alpha.5", "electron-builder": "24.6.3", diff --git a/plugins/adblocker/back.ts b/plugins/adblocker/back.ts index 1360ea46..f02f28a0 100644 --- a/plugins/adblocker/back.ts +++ b/plugins/adblocker/back.ts @@ -3,10 +3,9 @@ import { BrowserWindow } from 'electron'; import { loadAdBlockerEngine } from './blocker'; import config from './config'; -import pluginConfig from '../../config'; +import type { ConfigType } from '../../config/dynamic'; -const AdBlockOptionsObj = pluginConfig.get('plugins.adblocker'); -type AdBlockOptions = typeof AdBlockOptionsObj; +type AdBlockOptions = ConfigType<'adblocker'>; export default async (win: BrowserWindow, options: AdBlockOptions) => { if (await config.shouldUseBlocklists()) { diff --git a/plugins/captions-selector/front.ts b/plugins/captions-selector/front.ts index cfa61c1b..ee9adc53 100644 --- a/plugins/captions-selector/front.ts +++ b/plugins/captions-selector/front.ts @@ -7,7 +7,7 @@ import configProvider from './config'; import { ElementFromFile, templatePath } from '../utils'; import { YoutubePlayer } from '../../types/youtube-player'; -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; interface LanguageOptions { displayName: string; diff --git a/plugins/crossfade/front.ts b/plugins/crossfade/front.ts index aeff1064..203ddbb0 100644 --- a/plugins/crossfade/front.ts +++ b/plugins/crossfade/front.ts @@ -10,7 +10,7 @@ import { VolumeFader } from './fader'; import configProvider from './config'; import defaultConfigs from '../../config/defaults'; -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; let transitionAudio: Howl; // Howler audio used to fade out the current music let firstVideo = true; diff --git a/plugins/crossfade/menu.ts b/plugins/crossfade/menu.ts index 93034d48..dd0057f2 100644 --- a/plugins/crossfade/menu.ts +++ b/plugins/crossfade/menu.ts @@ -6,7 +6,7 @@ import config from './config'; import promptOptions from '../../providers/prompt-options'; import configOptions from '../../config/defaults'; -import { ConfigType } from '../../config/dynamic'; +import type { ConfigType } from '../../config/dynamic'; const defaultOptions = configOptions.plugins.crossfade; diff --git a/plugins/discord/back.ts b/plugins/discord/back.ts index f0970c3a..a505934d 100644 --- a/plugins/discord/back.ts +++ b/plugins/discord/back.ts @@ -5,7 +5,7 @@ import { dev } from 'electron-is'; import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser'; import registerCallback from '../../providers/song-info'; -import pluginConfig from '../../config'; +import type { ConfigType } from '../../config/dynamic'; // Application ID registered by @Zo-Bro-23 const clientId = '1043858434585526382'; @@ -118,8 +118,7 @@ export const connect = (showError = false) => { let clearActivity: NodeJS.Timeout | undefined; let updateActivity: import('../../providers/song-info').SongInfoCallback; -const DiscordOptionsObj = pluginConfig.get('plugins.discord'); -type DiscordOptions = typeof DiscordOptionsObj; +type DiscordOptions = ConfigType<'discord'>; export default ( win: Electron.BrowserWindow, diff --git a/plugins/discord/menu.ts b/plugins/discord/menu.ts index 6723b3f6..a8ea14c6 100644 --- a/plugins/discord/menu.ts +++ b/plugins/discord/menu.ts @@ -7,14 +7,14 @@ import { clear, connect, isConnected, registerRefresh } from './back'; import { setMenuOptions } from '../../config/plugins'; import promptOptions from '../../providers/prompt-options'; import { singleton } from '../../providers/decorators'; -import config from '../../config'; + +import type { ConfigType } from '../../config/dynamic'; const registerRefreshOnce = singleton((refreshMenu: () => void) => { registerRefresh(refreshMenu); }); -const DiscordOptionsObj = config.get('plugins.discord'); -type DiscordOptions = typeof DiscordOptionsObj; +type DiscordOptions = ConfigType<'discord'>; export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => { registerRefreshOnce(refreshMenu); diff --git a/plugins/downloader/back.ts b/plugins/downloader/back.ts index f30f532a..90a201c8 100644 --- a/plugins/downloader/back.ts +++ b/plugins/downloader/back.ts @@ -411,7 +411,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) { const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); - const folder = getFolder(config.get('downloadFolder')); + const folder = getFolder(config.get('downloadFolder') ?? ''); const playlistFolder = join(folder, safePlaylistTitle); if (existsSync(playlistFolder)) { if (!config.get('skipExisting')) { diff --git a/plugins/downloader/menu.ts b/plugins/downloader/menu.ts index be5f8430..c49a6389 100644 --- a/plugins/downloader/menu.ts +++ b/plugins/downloader/menu.ts @@ -16,7 +16,7 @@ export default (): MenuTemplate => [ click() { const result = dialog.showOpenDialogSync({ properties: ['openDirectory', 'createDirectory'], - defaultPath: getFolder(config.get('downloadFolder')), + defaultPath: getFolder(config.get('downloadFolder') ?? ''), }); if (result) { config.set('downloadFolder', result[0]); diff --git a/plugins/in-app-menu/back.ts b/plugins/in-app-menu/back.ts index 3fd13edf..61a00cbd 100644 --- a/plugins/in-app-menu/back.ts +++ b/plugins/in-app-menu/back.ts @@ -13,7 +13,7 @@ setupTitlebar(); // Tracks menu visibility -module.exports = (win: BrowserWindow) => { +export default (win: BrowserWindow) => { // Css for custom scrollbar + disable drag area(was causing bugs) injectCSS(win.webContents, path.join(__dirname, 'style.css')); diff --git a/plugins/in-app-menu/front.ts b/plugins/in-app-menu/front.ts index 61c09387..ae5179c2 100644 --- a/plugins/in-app-menu/front.ts +++ b/plugins/in-app-menu/front.ts @@ -9,8 +9,8 @@ function $(selector: string) { return document.querySelector(selector); } -module.exports = () => { - const visible = () => Boolean($('.cet-menubar')?.firstChild); +export default () => { + const visible = () => !!($('.cet-menubar')?.firstChild); const bar = new Titlebar({ icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png', backgroundColor: Color.fromHex('#050505'), diff --git a/plugins/last-fm/back.ts b/plugins/last-fm/back.ts index 2a83d9ee..69a93cc6 100644 --- a/plugins/last-fm/back.ts +++ b/plugins/last-fm/back.ts @@ -4,10 +4,9 @@ import md5 from 'md5'; import { setOptions } from '../../config/plugins'; import registerCallback, { SongInfo } from '../../providers/song-info'; import defaultConfig from '../../config/defaults'; -import config from '../../config'; +import type { ConfigType } from '../../config/dynamic'; -const LastFMOptionsObj = config.get('plugins.last-fm'); -type LastFMOptions = typeof LastFMOptionsObj; +type LastFMOptions = ConfigType<'last-fm'>; interface LastFmData { method: string, @@ -188,4 +187,4 @@ const lastfm = async (_win: BrowserWindow, config: LastFMOptions) => { }); }; -module.exports = lastfm; +export default lastfm; diff --git a/plugins/lyrics-genius/back.ts b/plugins/lyrics-genius/back.ts index b443b2fb..79522d13 100644 --- a/plugins/lyrics-genius/back.ts +++ b/plugins/lyrics-genius/back.ts @@ -9,13 +9,13 @@ import { GetGeniusLyric } from './types'; import { cleanupName, SongInfo } from '../../providers/song-info'; import { injectCSS } from '../utils'; -import config from '../../config'; + +import type { ConfigType } from '../../config/dynamic'; const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u; let revRomanized = false; -const LyricGeniusTypeObj = config.get('plugins.lyric-genius'); -export type LyricGeniusType = typeof LyricGeniusTypeObj; +export type LyricGeniusType = ConfigType<'lyric-genius'>; export default (win: BrowserWindow, options: LyricGeniusType) => { if (options.romanizedLyrics) { diff --git a/plugins/lyrics-genius/menu.ts b/plugins/lyrics-genius/menu.ts index 6dc6cd70..9e5eb9dc 100644 --- a/plugins/lyrics-genius/menu.ts +++ b/plugins/lyrics-genius/menu.ts @@ -4,7 +4,7 @@ import { LyricGeniusType, toggleRomanized } from './back'; import { setOptions } from '../../config/plugins'; -module.exports = (win: BrowserWindow, options: LyricGeniusType) => [ +export default (_: BrowserWindow, options: LyricGeniusType) => [ { label: 'Romanized Lyrics', type: 'checkbox', diff --git a/plugins/navigation/back.js b/plugins/navigation/back.ts similarity index 65% rename from plugins/navigation/back.js rename to plugins/navigation/back.ts index c2436c0b..bdf7e77d 100644 --- a/plugins/navigation/back.js +++ b/plugins/navigation/back.ts @@ -1,15 +1,17 @@ -const path = require('node:path'); +import path from 'node:path'; -const { ACTIONS, CHANNEL } = require('./actions.ts'); +import { BrowserWindow } from 'electron'; -const { injectCSS, listenAction } = require('../utils'); +import { ACTIONS, CHANNEL } from './actions'; -function handle(win) { +import { injectCSS, listenAction } from '../utils'; + +export function handle(win: BrowserWindow) { injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => { win.webContents.send('navigation-css-ready'); }); - listenAction(CHANNEL, (event, action) => { + listenAction(CHANNEL, (_, action) => { switch (action) { case ACTIONS.NEXT: { if (win.webContents.canGoForward()) { @@ -34,4 +36,4 @@ function handle(win) { }); } -module.exports = handle; +export default handle; diff --git a/plugins/navigation/front.js b/plugins/navigation/front.ts similarity index 71% rename from plugins/navigation/front.js rename to plugins/navigation/front.ts index 30135e05..d4ffe80c 100644 --- a/plugins/navigation/front.js +++ b/plugins/navigation/front.ts @@ -1,8 +1,8 @@ -const { ipcRenderer } = require('electron'); +import { ipcRenderer } from 'electron'; -const { ElementFromFile, templatePath } = require('../utils'); +import { ElementFromFile, templatePath } from '../utils'; -function run() { +export function run() { ipcRenderer.on('navigation-css-ready', () => { const forwardButton = ElementFromFile( templatePath(__dirname, 'forward.html'), @@ -16,4 +16,4 @@ function run() { }); } -module.exports = run; +export default run; diff --git a/plugins/no-google-login/back.js b/plugins/no-google-login/back.js deleted file mode 100644 index 897e8bc3..00000000 --- a/plugins/no-google-login/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/no-google-login/back.ts b/plugins/no-google-login/back.ts new file mode 100644 index 00000000..5634e5ef --- /dev/null +++ b/plugins/no-google-login/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/no-google-login/front.js b/plugins/no-google-login/front.ts similarity index 92% rename from plugins/no-google-login/front.js rename to plugins/no-google-login/front.ts index 57ac376d..b0f4158d 100644 --- a/plugins/no-google-login/front.js +++ b/plugins/no-google-login/front.ts @@ -18,7 +18,7 @@ function removeLoginElements() { const menuEntries = document.querySelectorAll( '#items ytmusic-guide-entry-renderer', ); - for (const item of menuEntries) { + menuEntries.forEach((item) => { const icon = item.querySelector('path'); if (icon) { observer.disconnect(); @@ -26,7 +26,7 @@ function removeLoginElements() { item.remove(); } } - } + }); }); observer.observe(document.documentElement, { childList: true, @@ -34,4 +34,4 @@ function removeLoginElements() { }); } -module.exports = removeLoginElements; +export default removeLoginElements; diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js deleted file mode 100644 index 867f65e6..00000000 --- a/plugins/notifications/back.js +++ /dev/null @@ -1,49 +0,0 @@ -const { Notification } = require('electron'); -const is = require('electron-is'); - -const { notificationImage } = require('./utils'); -const config = require('./config'); - -const registerCallback = require('../../providers/song-info'); - -const notify = (info) => { - // Fill the notification with content - const notification = { - title: info.title || 'Playing', - body: info.artist, - icon: notificationImage(info), - silent: true, - urgency: config.get('urgency'), - }; - - // Send the notification - const currentNotification = new Notification(notification); - currentNotification.show(); - - return currentNotification; -}; - -const setup = () => { - let oldNotification; - let currentUrl; - - registerCallback((songInfo) => { - if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) { - // Close the old notification - oldNotification?.close(); - currentUrl = songInfo.url; - // This fixes a weird bug that would cause the notification to be updated instead of showing - setTimeout(() => { - oldNotification = notify(songInfo); - }, 10); - } - }); -}; - -/** @param {Electron.BrowserWindow} win */ -module.exports = (win, options) => { - // Register the callback for new song information - is.windows() && options.interactive - ? require('./interactive')(win) - : setup(); -}; diff --git a/plugins/notifications/back.ts b/plugins/notifications/back.ts new file mode 100644 index 00000000..bc73819d --- /dev/null +++ b/plugins/notifications/back.ts @@ -0,0 +1,51 @@ +import { BrowserWindow, Notification } from 'electron'; + +import is from 'electron-is'; + +import { notificationImage } from './utils'; +import config from './config'; +import interactive from './interactive'; + +import registerCallback, { SongInfo } from '../../providers/song-info'; + +import type { ConfigType } from '../../config/dynamic'; + +type NotificationOptions = ConfigType<'notifications'>; + +const notify = (info: SongInfo) => { + // Send the notification + const currentNotification = new Notification({ + title: info.title || 'Playing', + body: info.artist, + icon: notificationImage(info), + silent: true, + urgency: config.get('urgency') as 'normal' | 'critical' | 'low', + }); + currentNotification.show(); + + return currentNotification; +}; + +const setup = () => { + let oldNotification: Notification; + let currentUrl: string | undefined; + + registerCallback((songInfo: SongInfo) => { + if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) { + // Close the old notification + oldNotification?.close(); + currentUrl = songInfo.url; + // This fixes a weird bug that would cause the notification to be updated instead of showing + setTimeout(() => { + oldNotification = notify(songInfo); + }, 10); + } + }); +}; + +export default (win: BrowserWindow, options: NotificationOptions) => { + // Register the callback for new song information + is.windows() && options.interactive + ? interactive(win) + : setup(); +}; diff --git a/plugins/notifications/config.js b/plugins/notifications/config.js deleted file mode 100644 index 2bd87052..00000000 --- a/plugins/notifications/config.js +++ /dev/null @@ -1,5 +0,0 @@ -const { PluginConfig } = require('../../config/dynamic'); - -const config = new PluginConfig('notifications'); - -module.exports = { ...config }; diff --git a/plugins/notifications/config.ts b/plugins/notifications/config.ts new file mode 100644 index 00000000..91244414 --- /dev/null +++ b/plugins/notifications/config.ts @@ -0,0 +1,5 @@ +import { PluginConfig } from '../../config/dynamic'; + +const config = new PluginConfig('notifications'); + +export default { ...config } as PluginConfig<'notifications'>; diff --git a/plugins/notifications/interactive.js b/plugins/notifications/interactive.ts similarity index 71% rename from plugins/notifications/interactive.js rename to plugins/notifications/interactive.ts index bb0fdc94..ab724ba2 100644 --- a/plugins/notifications/interactive.js +++ b/plugins/notifications/interactive.ts @@ -1,38 +1,32 @@ -const path = require('node:path'); +import path from 'node:path'; -const { Notification, app, ipcMain } = require('electron'); +import { app, BrowserWindow, ipcMain, Notification } from 'electron'; -const { notificationImage, icons, saveTempIcon, secondsToMinutes, ToastStyles } = require('./utils'); +import { icons, notificationImage, saveTempIcon, secondsToMinutes, ToastStyles } from './utils'; +import config from './config'; -/** - * @type {PluginConfig} - */ -const config = require('./config'); +import getSongControls from '../../providers/song-controls'; +import registerCallback, { SongInfo } from '../../providers/song-info'; +import { changeProtocolHandler } from '../../providers/protocol-handler'; +import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray'; -const getSongControls = require('../../providers/song-controls'); -const registerCallback = require('../../providers/song-info'); -const { changeProtocolHandler } = require('../../providers/protocol-handler'); -const { setTrayOnClick, setTrayOnDoubleClick } = require('../../tray'); +let songControls: ReturnType; +let savedNotification: Notification | undefined; - -let songControls; -let savedNotification; - -/** @param {Electron.BrowserWindow} win */ -module.exports = (win) => { +export default (win: BrowserWindow) => { songControls = getSongControls(win); let currentSeconds = 0; ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener')); - ipcMain.on('timeChanged', (_, t) => currentSeconds = t); + ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t); if (app.isPackaged) { saveTempIcon(); } - let savedSongInfo; - let lastUrl; + let savedSongInfo: SongInfo; + let lastUrl: string | undefined; // Register songInfoCallback registerCallback((songInfo) => { @@ -78,7 +72,7 @@ module.exports = (win) => { changeProtocolHandler( (cmd) => { if (Object.keys(songControls).includes(cmd)) { - songControls[cmd](); + songControls[cmd as keyof typeof songControls](); if (config.get('refreshOnPlayPause') && ( cmd === 'pause' || (cmd === 'play' && !config.get('unpauseNotification')) @@ -97,11 +91,18 @@ module.exports = (win) => { ); }; -function sendNotification(songInfo) { +function sendNotification(songInfo: SongInfo) { const iconSrc = notificationImage(songInfo); savedNotification?.close(); + let icon: string; + if (typeof iconSrc === 'object') { + icon = iconSrc.toDataURL(); + } else { + icon = iconSrc; + } + savedNotification = new Notification({ title: songInfo.title || 'Playing', body: songInfo.artist, @@ -111,7 +112,7 @@ function sendNotification(songInfo) { // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml // https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype - toastXml: getXml(songInfo, iconSrc), + toastXml: getXml(songInfo, icon), }); savedNotification.on('close', () => { @@ -121,7 +122,7 @@ function sendNotification(songInfo) { savedNotification.show(); } -const getXml = (songInfo, iconSrc) => { +const getXml = (songInfo: SongInfo, iconSrc: string) => { switch (config.get('toastStyle')) { default: case ToastStyles.logo: @@ -155,7 +156,7 @@ const iconLocation = app.isPackaged ? path.resolve(app.getPath('userData'), 'icons') : path.resolve(__dirname, '..', '..', 'assets/media-icons-black'); -const display = (kind) => { +const display = (kind: keyof typeof icons) => { if (config.get('toastStyle') === ToastStyles.legacy) { return `content="${icons[kind]}"`; } @@ -166,10 +167,10 @@ const display = (kind) => { `; }; -const getButton = (kind) => +const getButton = (kind: keyof typeof icons) => ``; -const getButtons = (isPaused) => `\ +const getButtons = (isPaused: boolean) => `\ ${getButton('previous')} ${isPaused ? getButton('play') : getButton('pause')} @@ -177,7 +178,7 @@ const getButtons = (isPaused) => `\ \ `; -const toast = (content, isPaused) => `\ +const toast = (content: string, isPaused: boolean) => `\ `; -const xmlImage = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\ +const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\ ${title} ${artist}\ -`, isPaused); +`, isPaused ?? false); -const xmlLogo = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"'); +const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"'); -const xmlHero = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="hero"'); +const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"'); -const xmlBannerBottom = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, ''); +const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, ''); -const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\ +const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => toast(`\ @@ -211,17 +212,17 @@ const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\ ${xmlMoreData(songInfo)} \ -`, songInfo.isPaused); +`, songInfo.isPaused ?? false); -const xmlMoreData = ({ album, elapsedSeconds, songDuration }) => `\ +const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\ ${album ? `${album}` : ''} - ${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)} + ${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)} \ `; -const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`\ +const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\ @@ -230,9 +231,9 @@ const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(` \ -`, isPaused); +`, isPaused ?? false); -const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\ +const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\ @@ -241,9 +242,9 @@ const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\ ${artist} \ -`, isPaused); +`, isPaused ?? false); -const titleFontPicker = (title) => { +const titleFontPicker = (title: string) => { if (title.length <= 13) { return 'Header'; } diff --git a/plugins/notifications/menu.js b/plugins/notifications/menu.ts similarity index 68% rename from plugins/notifications/menu.js rename to plugins/notifications/menu.ts index 8886f3c3..48bbaafd 100644 --- a/plugins/notifications/menu.js +++ b/plugins/notifications/menu.ts @@ -1,9 +1,14 @@ -const is = require('electron-is'); +import is from 'electron-is'; -const { urgencyLevels, ToastStyles, snakeToCamel } = require('./utils'); -const config = require('./config'); +import {BrowserWindow, MenuItem} from 'electron'; -module.exports = (_win, options) => [ +import { snakeToCamel, ToastStyles, urgencyLevels } from './utils'; + +import config from './config'; + +import type { ConfigType } from '../../config/dynamic'; + +export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [ ...(is.linux() ? [ { @@ -24,7 +29,7 @@ module.exports = (_win, options) => [ type: 'checkbox', checked: options.interactive, // Doesn't update until restart - click: (item) => config.setAndMaybeRestart('interactive', item.checked), + click: (item: MenuItem) => config.setAndMaybeRestart('interactive', item.checked), }, { // Submenu with settings for interactive notifications (name shouldn't be too long) @@ -34,19 +39,19 @@ module.exports = (_win, options) => [ label: 'Open/Close on tray click', type: 'checkbox', checked: options.trayControls, - click: (item) => config.set('trayControls', item.checked), + click: (item: MenuItem) => config.set('trayControls', item.checked), }, { label: 'Hide Button Text', type: 'checkbox', checked: options.hideButtonText, - click: (item) => config.set('hideButtonText', item.checked), + click: (item: MenuItem) => config.set('hideButtonText', item.checked), }, { label: 'Refresh on Play/Pause', type: 'checkbox', checked: options.refreshOnPlayPause, - click: (item) => config.set('refreshOnPlayPause', item.checked), + click: (item: MenuItem) => config.set('refreshOnPlayPause', item.checked), }, ], }, @@ -60,11 +65,11 @@ module.exports = (_win, options) => [ label: 'Show notification on unpause', type: 'checkbox', checked: options.unpauseNotification, - click: (item) => config.set('unpauseNotification', item.checked), + click: (item: MenuItem) => config.set('unpauseNotification', item.checked), }, ]; -function getToastStyleMenuItems(options) { +export function getToastStyleMenuItems(options: ConfigType<'notifications'>) { const array = Array.from({ length: Object.keys(ToastStyles).length }); // ToastStyles index starts from 1 diff --git a/plugins/notifications/utils.js b/plugins/notifications/utils.ts similarity index 62% rename from plugins/notifications/utils.js rename to plugins/notifications/utils.ts index 6158bb3f..7870730c 100644 --- a/plugins/notifications/utils.js +++ b/plugins/notifications/utils.ts @@ -1,19 +1,20 @@ -const path = require('node:path'); +import path from 'node:path'; +import fs from 'node:fs'; -const fs = require('node:fs'); +import { app, NativeImage } from 'electron'; -const { app } = require('electron'); +import config from './config'; -const config = require('./config'); +import { cache } from '../../providers/decorators'; +import { SongInfo } from '../../providers/song-info'; const icon = 'assets/youtube-music.png'; const userData = app.getPath('userData'); const temporaryIcon = path.join(userData, 'tempIcon.png'); const temporaryBanner = path.join(userData, 'tempBanner.png'); -const { cache } = require('../../providers/decorators'); -module.exports.ToastStyles = { +export const ToastStyles = { logo: 1, banner_centered_top: 2, hero: 3, @@ -23,20 +24,20 @@ module.exports.ToastStyles = { legacy: 7, }; -module.exports.icons = { +export const icons = { play: '\u{1405}', // ᐅ pause: '\u{2016}', // ‖ next: '\u{1433}', // ᐳ previous: '\u{1438}', // ᐸ }; -module.exports.urgencyLevels = [ +export const urgencyLevels = [ { name: 'Low', value: 'low' }, { name: 'Normal', value: 'normal' }, { name: 'High', value: 'critical' }, ]; -const nativeImageToLogo = cache((nativeImage) => { +const nativeImageToLogo = cache((nativeImage: NativeImage) => { const temporaryImage = nativeImage.resize({ height: 256 }); const margin = Math.max(temporaryImage.getSize().width - 256, 0); @@ -48,7 +49,7 @@ const nativeImageToLogo = cache((nativeImage) => { }); }); -module.exports.notificationImage = (songInfo) => { +export const notificationImage = (songInfo: SongInfo) => { if (!songInfo.image) { return icon; } @@ -58,30 +59,30 @@ module.exports.notificationImage = (songInfo) => { } switch (config.get('toastStyle')) { - case module.exports.ToastStyles.logo: - case module.exports.ToastStyles.legacy: { - return this.saveImage(nativeImageToLogo(songInfo.image), temporaryIcon); + case ToastStyles.logo: + case ToastStyles.legacy: { + return saveImage(nativeImageToLogo(songInfo.image), temporaryIcon); } default: { - return this.saveImage(songInfo.image, temporaryBanner); + return saveImage(songInfo.image, temporaryBanner); } } }; -module.exports.saveImage = cache((img, savePath) => { +export const saveImage = cache((img: NativeImage, savePath: string) => { try { fs.writeFileSync(savePath, img.toPNG()); - } catch (error) { - console.log(`Error writing song icon to disk:\n${error.toString()}`); + } catch (error: unknown) { + console.log(`Error writing song icon to disk:\n${String(error)}`); return icon; } return savePath; }); -module.exports.saveTempIcon = () => { - for (const kind of Object.keys(module.exports.icons)) { +export const saveTempIcon = () => { + for (const kind of Object.keys(icons)) { const destinationPath = path.join(userData, 'icons', `${kind}.png`); if (fs.existsSync(destinationPath)) { continue; @@ -94,13 +95,13 @@ module.exports.saveTempIcon = () => { } }; -module.exports.snakeToCamel = (string_) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) => +export const snakeToCamel = (string_: string) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) => group.toUpperCase() .replace('-', ' ') .replace('_', ' '), ); -module.exports.secondsToMinutes = (seconds) => { +export const secondsToMinutes = (seconds: number) => { const minutes = Math.floor(seconds / 60); const secondsLeft = seconds % 60; return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`; diff --git a/plugins/picture-in-picture/back.ts b/plugins/picture-in-picture/back.ts index 1a588649..2041e404 100644 --- a/plugins/picture-in-picture/back.ts +++ b/plugins/picture-in-picture/back.ts @@ -5,7 +5,7 @@ import { app, BrowserWindow, ipcMain } from 'electron'; import { setOptions as setPluginOptions } from '../../config/plugins'; import { injectCSS } from '../utils'; -import config from '../../config'; +import type { ConfigType } from '../../config/dynamic'; let isInPiP = false; let originalPosition: number[]; @@ -15,9 +15,7 @@ let originalMaximized: boolean; let win: BrowserWindow; -// Magic of TypeScript -const PiPOptionsObj = config.get('plugins.picture-in-picture'); -type PiPOptions = typeof PiPOptionsObj; +type PiPOptions = ConfigType<'picture-in-picture'>; let options: Partial; diff --git a/plugins/picture-in-picture/front.ts b/plugins/picture-in-picture/front.ts index a4f83d72..88cdebed 100644 --- a/plugins/picture-in-picture/front.ts +++ b/plugins/picture-in-picture/front.ts @@ -2,31 +2,44 @@ import { ipcRenderer } from 'electron'; import { toKeyEvent } from 'keyboardevent-from-electron-accelerator'; import keyEventAreEqual from 'keyboardevents-areequal'; -const { getSongMenu } = require('../../providers/dom-elements'); -const { ElementFromFile, templatePath } = require('../utils'); +import { getSongMenu } from '../../providers/dom-elements'; -function $(selector) { +import { ElementFromFile, templatePath } from '../utils'; + +import type { ConfigType } from '../../config/dynamic'; + +type PiPOptions = ConfigType<'picture-in-picture'>; + +function $(selector: string) { return document.querySelector(selector); } let useNativePiP = false; -let menu = null; +let menu: Element | null = null; const pipButton = ElementFromFile( templatePath(__dirname, 'picture-in-picture.html'), ); // Will also clone -function replaceButton(query, button) { - const svg = button.querySelector('#icon svg').cloneNode(true); - button.replaceWith(button.cloneNode(true)); - button.remove(); - const newButton = $(query); - newButton.querySelector('#icon').append(svg); - return newButton; +function replaceButton(query: string, button: Element) { + const svg = button.querySelector('#icon svg')?.cloneNode(true); + if (svg) { + button.replaceWith(button.cloneNode(true)); + button.remove(); + const newButton = $(query); + if (newButton) { + newButton.querySelector('#icon')?.append(svg); + } + return newButton; + } + return null; } -function cloneButton(query) { - replaceButton(query, $(query)); +function cloneButton(query: string) { + const button = $(query); + if (button) { + replaceButton(query, button); + } return $(query); } @@ -38,13 +51,18 @@ const observer = new MutationObserver(() => { } } - if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) { + if ( + menu.contains(pipButton) || + !(menu.parentElement as (HTMLElement & { eventSink_: Element }) | null) + ?.eventSink_ + ?.matches('ytmusic-menu-renderer.ytmusic-player-bar') + ) { return; } - const menuUrl = $( + const menuUrl = ($( 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint', - )?.href; + ) as HTMLAnchorElement)?.href; if (menuUrl && !menuUrl.includes('watch?')) { return; } @@ -55,15 +73,15 @@ const observer = new MutationObserver(() => { const togglePictureInPicture = async () => { if (useNativePiP) { const isInPiP = document.pictureInPictureElement !== null; - const video = $('video'); + const video = $('video') as HTMLVideoElement | null; const togglePiP = () => isInPiP ? document.exitPictureInPicture.call(document) - : video.requestPictureInPicture.call(video); + : video?.requestPictureInPicture?.call(video); try { await togglePiP(); - $('#icon').click(); // Close the menu + ($('#icon') as HTMLButtonElement | null)?.click(); // Close the menu return true; } catch { } @@ -72,24 +90,26 @@ const togglePictureInPicture = async () => { ipcRenderer.send('picture-in-picture'); return false; }; -global.togglePictureInPicture = togglePictureInPicture; +// For UI (HTML) +// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access +(global as any).togglePictureInPicture = togglePictureInPicture; const listenForToggle = () => { - const originalExitButton = $('.exit-fullscreen-button'); - const appLayout = $('ytmusic-app-layout'); - const expandMenu = $('#expanding-menu'); - const middleControls = $('.middle-controls'); - const playerPage = $('ytmusic-player-page'); - const togglePlayerPageButton = $('.toggle-player-page-button'); - const fullScreenButton = $('.fullscreen-button'); - const player = $('#player'); - const onPlayerDblClick = player.onDoubleClick_; + const originalExitButton = $('.exit-fullscreen-button') as HTMLButtonElement; + const appLayout = $('ytmusic-app-layout') as HTMLElement; + const expandMenu = $('#expanding-menu') as HTMLElement; + const middleControls = $('.middle-controls') as HTMLButtonElement; + const playerPage = $('ytmusic-player-page') as HTMLElement & { playerPageOpen_: boolean }; + const togglePlayerPageButton = $('.toggle-player-page-button') as HTMLButtonElement; + const fullScreenButton = $('.fullscreen-button') as HTMLButtonElement; + const player = ($('#player') as (HTMLVideoElement & { onDoubleClick_: () => void | undefined })); + const onPlayerDblClick = player?.onDoubleClick_; - const titlebar = $('.cet-titlebar'); + const titlebar = $('.cet-titlebar') as HTMLElement; - ipcRenderer.on('pip-toggle', (_, isPip) => { + ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { if (isPip) { - replaceButton('.exit-fullscreen-button', originalExitButton).addEventListener('click', () => togglePictureInPicture()); + replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture()); player.onDoubleClick_ = () => { }; @@ -104,9 +124,9 @@ const listenForToggle = () => { titlebar.style.display = 'none'; } } else { - $('.exit-fullscreen-button').replaceWith(originalExitButton); + $('.exit-fullscreen-button')?.replaceWith(originalExitButton); player.onDoubleClick_ = onPlayerDblClick; - expandMenu.onmouseleave = undefined; + expandMenu.onmouseleave = null; originalExitButton.click(); appLayout.classList.remove('pip'); if (titlebar) { @@ -116,22 +136,23 @@ const listenForToggle = () => { }); }; -function observeMenu(options) { +function observeMenu(options: PiPOptions) { useNativePiP = options.useNativePiP; document.addEventListener( 'apiLoaded', () => { listenForToggle(); - cloneButton('.player-minimize-button').addEventListener('click', async () => { + cloneButton('.player-minimize-button')?.addEventListener('click', async () => { await togglePictureInPicture(); - setTimeout(() => $('#player').click()); + setTimeout(() => ($('#player') as HTMLButtonElement | undefined)?.click()); }); // Allows easily closing the menu by programmatically clicking outside of it - $('#expanding-menu').removeAttribute('no-cancel-on-outside-click'); + $('#expanding-menu')?.removeAttribute('no-cancel-on-outside-click'); // TODO: think about wether an additional button in songMenu is needed - observer.observe($('ytmusic-popup-container'), { + const popupContainer = $('ytmusic-popup-container'); + if (popupContainer) observer.observe(popupContainer, { childList: true, subtree: true, }); @@ -140,7 +161,7 @@ function observeMenu(options) { ); } -module.exports = (options) => { +export default (options: PiPOptions) => { observeMenu(options); if (options.hotkey) { @@ -148,7 +169,7 @@ module.exports = (options) => { window.addEventListener('keydown', (event) => { if ( keyEventAreEqual(event, hotkeyEvent) - && !$('ytmusic-search-box').opened + && !($('ytmusic-search-box') as (HTMLElement & { opened: boolean }) | undefined)?.opened ) { togglePictureInPicture(); } diff --git a/plugins/picture-in-picture/keyboardevent-from-electron-accelerator.d.ts b/plugins/picture-in-picture/keyboardevent-from-electron-accelerator.d.ts new file mode 100644 index 00000000..67af6f51 --- /dev/null +++ b/plugins/picture-in-picture/keyboardevent-from-electron-accelerator.d.ts @@ -0,0 +1,12 @@ +declare module 'keyboardevent-from-electron-accelerator' { + interface KeyboardEvent { + key?: string; + code?: string; + metaKey?: boolean; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; + } + + export const toKeyEvent: (accelerator: string) => KeyboardEvent; +} diff --git a/plugins/picture-in-picture/keyboardevents-areequal.d.ts b/plugins/picture-in-picture/keyboardevents-areequal.d.ts new file mode 100644 index 00000000..ac219b1f --- /dev/null +++ b/plugins/picture-in-picture/keyboardevents-areequal.d.ts @@ -0,0 +1,14 @@ +declare module 'keyboardevents-areequal' { + interface KeyboardEvent { + key?: string; + code?: string; + metaKey?: boolean; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; + } + + const areEqual: (event1: KeyboardEvent, event2: KeyboardEvent) => boolean; + + export default areEqual; +} diff --git a/plugins/picture-in-picture/menu.ts b/plugins/picture-in-picture/menu.ts index 107e9b06..ff154698 100644 --- a/plugins/picture-in-picture/menu.ts +++ b/plugins/picture-in-picture/menu.ts @@ -1,10 +1,14 @@ -const prompt = require('custom-electron-prompt'); +import prompt from 'custom-electron-prompt'; -const { setOptions } = require('./back.ts'); +import { BrowserWindow } from 'electron'; -const promptOptions = require('../../providers/prompt-options'); +import { setOptions } from './back'; -module.exports = (win, options) => [ +import promptOptions from '../../providers/prompt-options'; +import type { ConfigType } from '../../config/dynamic'; +import { MenuTemplate } from '../../menu'; + +export default (win: BrowserWindow, options: ConfigType<'picture-in-picture'>): MenuTemplate => [ { label: 'Always on top', type: 'checkbox', @@ -33,7 +37,7 @@ module.exports = (win, options) => [ { label: 'Hotkey', type: 'checkbox', - checked: options.hotkey, + checked: !!options.hotkey, async click(item) { const output = await prompt({ title: 'Picture in Picture Hotkey', @@ -51,7 +55,7 @@ module.exports = (win, options) => [ const { value, accelerator } = output[0]; setOptions({ [value]: accelerator }); - item.checked = Boolean(accelerator); + item.checked = !!accelerator; } else { // Reset checkbox if prompt was canceled item.checked = !item.checked; diff --git a/plugins/playback-speed/front.js b/plugins/playback-speed/front.ts similarity index 52% rename from plugins/playback-speed/front.js rename to plugins/playback-speed/front.ts index 7c01208f..77bf85cb 100644 --- a/plugins/playback-speed/front.js +++ b/plugins/playback-speed/front.ts @@ -1,14 +1,15 @@ -const { getSongMenu } = require('../../providers/dom-elements'); -const { ElementFromFile, templatePath } = require('../utils'); -const { singleton } = require('../../providers/decorators'); +import { getSongMenu } from '../../providers/dom-elements'; +import { ElementFromFile, templatePath } from '../utils'; +import { singleton } from '../../providers/decorators'; -function $(selector) { + +function $(selector: string) { return document.querySelector(selector); } const slider = ElementFromFile(templatePath(__dirname, 'slider.html')); -const roundToTwo = (n) => Math.round(n * 1e2) / 1e2; +const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2; const MIN_PLAYBACK_SPEED = 0.07; const MAX_PLAYBACK_SPEED = 16; @@ -16,19 +17,19 @@ const MAX_PLAYBACK_SPEED = 16; let playbackSpeed = 1; const updatePlayBackSpeed = () => { - $('video').playbackRate = playbackSpeed; + ($('video') as HTMLVideoElement).playbackRate = playbackSpeed; const playbackSpeedElement = $('#playback-speed-value'); if (playbackSpeedElement) { - playbackSpeedElement.innerHTML = playbackSpeed; + playbackSpeedElement.innerHTML = String(playbackSpeed); } }; -let menu; +let menu: Element | null = null; const setupSliderListener = singleton(() => { - $('#playback-speed-slider').addEventListener('immediate-value-changed', (e) => { - playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED; + $('#playback-speed-slider')?.addEventListener('immediate-value-changed', (e) => { + playbackSpeed = (e as CustomEvent<{ value: number; }>).detail.value || MIN_PLAYBACK_SPEED; if (isNaN(playbackSpeed)) { playbackSpeed = 1; } @@ -43,20 +44,28 @@ const observePopupContainer = () => { menu = getSongMenu(); } - if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) { + if ( + menu && + (menu.parentElement as HTMLElement & { eventSink_: Element | null }) + ?.eventSink_ + ?.matches('ytmusic-menu-renderer.ytmusic-player-bar')&& !menu.contains(slider) + ) { menu.prepend(slider); setupSliderListener(); } }); - observer.observe($('ytmusic-popup-container'), { - childList: true, - subtree: true, - }); + const popupContainer = $('ytmusic-popup-container'); + if (popupContainer) { + observer.observe(popupContainer, { + childList: true, + subtree: true, + }); + } }; const observeVideo = () => { - const video = $('video');+ + const video = $('video') as HTMLVideoElement; video.addEventListener('ratechange', forcePlaybackRate); video.addEventListener('srcChanged', forcePlaybackRate); }; @@ -76,17 +85,18 @@ const setupWheelListener = () => { updatePlayBackSpeed(); // Update slider position - $('#playback-speed-slider').value = playbackSpeed; + ($('#playback-speed-slider') as HTMLElement & { value: number }).value = playbackSpeed; }); }; -function forcePlaybackRate(e) { - if (e.target.playbackRate !== playbackSpeed) { - e.target.playbackRate = playbackSpeed; +function forcePlaybackRate(e: Event) { + const videoElement = (e.target as HTMLVideoElement); + if (videoElement.playbackRate !== playbackSpeed) { + videoElement.playbackRate = playbackSpeed; } } -module.exports = () => { +export default () => { document.addEventListener('apiLoaded', () => { observePopupContainer(); observeVideo(); diff --git a/plugins/precise-volume/back.js b/plugins/precise-volume/back.ts similarity index 55% rename from plugins/precise-volume/back.js rename to plugins/precise-volume/back.ts index e9215973..e4ef4ea9 100644 --- a/plugins/precise-volume/back.js +++ b/plugins/precise-volume/back.ts @@ -1,17 +1,20 @@ -const path = require('node:path'); +import path from 'node:path'; -const { globalShortcut } = require('electron'); +import { globalShortcut, BrowserWindow } from 'electron'; -const { injectCSS } = require('../utils'); +import { injectCSS } from '../utils'; +import type { ConfigType } from '../../config/dynamic'; /* This is used to determine if plugin is actually active -(not if its only enabled in options) +(not if it's only enabled in options) */ -let enabled = false; +let isEnabled = false; -module.exports = (win, options) => { - enabled = true; +export const enabled = () => isEnabled; + +export default (win: BrowserWindow, options: ConfigType<'precise-volume'>) => { + isEnabled = true; injectCSS(win.webContents, path.join(__dirname, 'volume-hud.css')); if (options.globalShortcuts?.volumeUp) { @@ -22,5 +25,3 @@ module.exports = (win, options) => { globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false)); } }; - -module.exports.enabled = () => enabled; diff --git a/plugins/precise-volume/front.js b/plugins/precise-volume/front.ts similarity index 64% rename from plugins/precise-volume/front.js rename to plugins/precise-volume/front.ts index a8e8f818..5dad588c 100644 --- a/plugins/precise-volume/front.js +++ b/plugins/precise-volume/front.ts @@ -1,22 +1,25 @@ -const { ipcRenderer } = require('electron'); +import { ipcRenderer } from 'electron'; -const { setOptions, setMenuOptions, isEnabled } = require('../../config/plugins'); +import { setOptions, setMenuOptions, isEnabled } from '../../config/plugins'; +import { debounce } from '../../providers/decorators'; -function $(selector) { +import { YoutubePlayer } from '../../types/youtube-player'; + +import type { ConfigType } from '../../config/dynamic'; + +function $(selector: string) { return document.querySelector(selector); } -const { debounce } = require('../../providers/decorators'); +let api: YoutubePlayer; +let options: ConfigType<'precise-volume'>; -let api; -let options; - -module.exports = (_options) => { +export default (_options: ConfigType<'precise-volume'>) => { options = _options; document.addEventListener('apiLoaded', (e) => { api = e.detail; - ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease)); - ipcRenderer.on('setVolume', (_, value) => setVolume(value)); + ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease)); + ipcRenderer.on('setVolume', (_, value: number) => setVolume(value)); firstRun(); }, { once: true, passive: true }); }; @@ -26,23 +29,22 @@ const writeOptions = debounce(() => { setOptions('precise-volume', options); }, 1000); -const moveVolumeHud = debounce((showVideo) => { - const volumeHud = $('#volumeHud'); +export const moveVolumeHud = debounce((showVideo: boolean) => { + const volumeHud = $('#volumeHud') as HTMLElement | undefined; if (!volumeHud) { return; } volumeHud.style.top = showVideo - ? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px` - : 0; + ? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px` + : '0'; }, 250); -module.exports.moveVolumeHud = moveVolumeHud; -const hideVolumeHud = debounce((volumeHud) => { - volumeHud.style.opacity = 0; +const hideVolumeHud = debounce((volumeHud: HTMLElement) => { + volumeHud.style.opacity = '0'; }, 2000); -const hideVolumeSlider = debounce((slider) => { +const hideVolumeSlider = debounce((slider: HTMLElement) => { slider.classList.remove('on-hover'); }, 2500); @@ -61,14 +63,15 @@ function firstRun() { setupLocalArrowShortcuts(); - const noVid = $('#main-panel')?.computedStyleMap().get('display').value === 'none'; + // Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue + const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none'; injectVolumeHud(noVid); if (!noVid) { setupVideoPlayerOnwheel(); if (!isEnabled('video-toggle')) { // Video-toggle handles hud positioning on its own const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; - $('video').addEventListener('srcChanged', () => moveVolumeHud(videoMode())); + $('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode())); } } @@ -79,51 +82,55 @@ function firstRun() { }); } -function injectVolumeHud(noVid) { +function injectVolumeHud(noVid: boolean) { if (noVid) { const position = 'top: 18px; right: 60px;'; const mainStyle = 'font-size: xx-large;'; - $('.center-content.ytmusic-nav-bar').insertAdjacentHTML('beforeend', + $('.center-content.ytmusic-nav-bar')?.insertAdjacentHTML('beforeend', ``); } else { const position = 'top: 10px; left: 10px;'; const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;'; - $('#song-video').insertAdjacentHTML('afterend', + $('#song-video')?.insertAdjacentHTML('afterend', ``); } } -function showVolumeHud(volume) { - const volumeHud = $('#volumeHud'); +function showVolumeHud(volume: number) { + const volumeHud = $('#volumeHud') as HTMLElement | undefined; if (!volumeHud) { return; } volumeHud.textContent = `${volume}%`; - volumeHud.style.opacity = 1; + volumeHud.style.opacity = '1'; hideVolumeHud(volumeHud); } /** Add onwheel event to video player */ function setupVideoPlayerOnwheel() { - $('#main-panel').addEventListener('wheel', (event) => { + const panel = $('#main-panel') as HTMLElement | undefined; + if (!panel) return; + + panel.addEventListener('wheel', (event) => { event.preventDefault(); // Event.deltaY < 0 means wheel-up changeVolume(event.deltaY < 0); }); } -function saveVolume(volume) { +function saveVolume(volume: number) { options.savedVolume = volume; writeOptions(); } /** Add onwheel event to play bar and also track if play bar is hovered */ function setupPlaybar() { - const playerbar = $('ytmusic-player-bar'); + const playerbar = $('ytmusic-player-bar') as HTMLElement | undefined; + if (!playerbar) return; playerbar.addEventListener('wheel', (event) => { event.preventDefault(); @@ -148,23 +155,28 @@ function setupSliderObserver() { const sliderObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { // This checks that volume-slider was manually set - if (mutation.oldValue !== mutation.target.value - && (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - mutation.target.value) > 4)) { + const target = mutation.target as HTMLInputElement; + const targetValueNumeric = Number(target.value); + if (mutation.oldValue !== target.value + && (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) { // Diff>4 means it was manually set - setTooltip(mutation.target.value); - saveVolume(mutation.target.value); + setTooltip(targetValueNumeric); + saveVolume(targetValueNumeric); } } }); + const slider = $('#volume-slider'); + if (!slider) return; + // Observing only changes in 'value' of volume-slider - sliderObserver.observe($('#volume-slider'), { + sliderObserver.observe(slider, { attributeFilter: ['value'], attributeOldValue: true, }); } -function setVolume(value) { +function setVolume(value: number) { api.setVolume(value); // Save the new volume saveVolume(value); @@ -181,7 +193,7 @@ function setVolume(value) { } /** If (toIncrease = false) then volume decrease */ -function changeVolume(toIncrease) { +function changeVolume(toIncrease: boolean) { // Apply volume change if valid const steps = Number(options.steps || 1); setVolume(toIncrease @@ -190,17 +202,20 @@ function changeVolume(toIncrease) { } function updateVolumeSlider() { + const savedVolume = options.savedVolume ?? 0; // Slider value automatically rounds to multiples of 5 for (const slider of ['#volume-slider', '#expand-volume-slider']) { - $(slider).value - = options.savedVolume > 0 && options.savedVolume < 5 + ($(slider) as HTMLInputElement).value + = String(savedVolume > 0 && savedVolume < 5 ? 5 - : options.savedVolume; + : savedVolume); } } function showVolumeSlider() { - const slider = $('#volume-slider'); + const slider = $('#volume-slider') as HTMLElement | null; + if (!slider) return; + // This class display the volume slider if not in minimized mode slider.classList.add('on-hover'); @@ -215,16 +230,16 @@ const tooltipTargets = [ '#expand-volume', ]; -function setTooltip(volume) { +function setTooltip(volume: number) { for (const target of tooltipTargets) { - $(target).title = `${volume}%`; + ($(target) as HTMLElement).title = `${volume}%`; } } function setupLocalArrowShortcuts() { if (options.arrowsShortcut) { window.addEventListener('keydown', (event) => { - if ($('ytmusic-search-box').opened) { + if (($('ytmusic-search-box') as (HTMLElement & { opened: boolean }) | null)?.opened) { return; } diff --git a/plugins/precise-volume/menu.js b/plugins/precise-volume/menu.ts similarity index 60% rename from plugins/precise-volume/menu.js rename to plugins/precise-volume/menu.ts index ea4c0ff5..de6dfceb 100644 --- a/plugins/precise-volume/menu.js +++ b/plugins/precise-volume/menu.ts @@ -1,15 +1,20 @@ -const prompt = require('custom-electron-prompt'); +import prompt, { KeybindOptions } from 'custom-electron-prompt'; -const { enabled } = require('./back'); +import { BrowserWindow, MenuItem } from 'electron'; -const { setMenuOptions } = require('../../config/plugins'); -const promptOptions = require('../../providers/prompt-options'); +import { enabled } from './back'; -function changeOptions(changedOptions, options, win) { +import { setMenuOptions } from '../../config/plugins'; +import promptOptions from '../../providers/prompt-options'; +import { MenuTemplate } from '../../menu'; + +import type { ConfigType } from '../../config/dynamic'; + +function changeOptions(changedOptions: Partial>, options: ConfigType<'precise-volume'>, win: BrowserWindow) { for (const option in changedOptions) { - options[option] = changedOptions[option]; + // HACK: Weird TypeScript error + (options as Record)[option] = (changedOptions as Record)[option]; } - // Dynamically change setting if plugin is enabled if (enabled()) { win.webContents.send('setOptions', changedOptions); @@ -18,7 +23,7 @@ function changeOptions(changedOptions, options, win) { } } -module.exports = (win, options) => [ +export default (win: BrowserWindow, options: ConfigType<'precise-volume'>): MenuTemplate => [ { label: 'Local Arrowkeys Controls', type: 'checkbox', @@ -40,9 +45,9 @@ module.exports = (win, options) => [ ]; // Helper function for globalShortcuts prompt -const kb = (label_, value_, default_) => ({ value: value_, label: label_, default: default_ || undefined }); +const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined }); -async function promptVolumeSteps(win, options) { +async function promptVolumeSteps(win: BrowserWindow, options: ConfigType<'precise-volume'>) { const output = await prompt({ title: 'Volume Steps', label: 'Choose Volume Increase/Decrease Steps', @@ -58,7 +63,7 @@ async function promptVolumeSteps(win, options) { } } -async function promptGlobalShortcuts(win, options, item) { +async function promptGlobalShortcuts(win: BrowserWindow, options: ConfigType<'precise-volume'>, item: MenuItem) { const output = await prompt({ title: 'Global Volume Keybinds', label: 'Choose Global Volume Keybinds:', @@ -71,9 +76,12 @@ async function promptGlobalShortcuts(win, options, item) { }, win); if (output) { - const newGlobalShortcuts = {}; + const newGlobalShortcuts: { + volumeUp: string; + volumeDown: string; + } = { volumeUp: '', volumeDown: '' }; for (const { value, accelerator } of output) { - newGlobalShortcuts[value] = accelerator; + newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator; } changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win); diff --git a/plugins/precise-volume/preload.js b/plugins/precise-volume/preload.js deleted file mode 100644 index ab16e6c1..00000000 --- a/plugins/precise-volume/preload.js +++ /dev/null @@ -1,32 +0,0 @@ -const is = require('electron-is'); - -let ignored = { - id: ['volume-slider', 'expand-volume-slider'], - types: ['mousewheel', 'keydown', 'keyup'], -}; - -function overrideAddEventListener() { - // Save native addEventListener - Element.prototype._addEventListener = Element.prototype.addEventListener; - // Override addEventListener to Ignore specific events in volume-slider - Element.prototype.addEventListener = function (type, listener, useCapture = false) { - if (!( - ignored.id.includes(this.id) - && ignored.types.includes(type) - )) { - this._addEventListener(type, listener, useCapture); - } else if (is.dev()) { - console.log(`Ignoring event: "${this.id}.${type}()"`); - } - }; -} - -module.exports = () => { - overrideAddEventListener(); - // Restore original function after finished loading to avoid keeping Element.prototype altered - window.addEventListener('load', () => { - Element.prototype.addEventListener = Element.prototype._addEventListener; - Element.prototype._addEventListener = undefined; - ignored = undefined; - }, { once: true }); -}; diff --git a/plugins/precise-volume/preload.ts b/plugins/precise-volume/preload.ts new file mode 100644 index 00000000..363ee527 --- /dev/null +++ b/plugins/precise-volume/preload.ts @@ -0,0 +1,41 @@ +/* what */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import is from 'electron-is'; + +const ignored = { + id: ['volume-slider', 'expand-volume-slider'], + types: ['mousewheel', 'keydown', 'keyup'], +}; + +function overrideAddEventListener() { + // YO WHAT ARE YOU DOING NOW?!?! + // Save native addEventListener + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/unbound-method + Element.prototype._addEventListener = Element.prototype.addEventListener; + // Override addEventListener to Ignore specific events in volume-slider + Element.prototype.addEventListener = function (type: string, listener: (event: Event) => void, useCapture = false) { + if (!( + ignored.id.includes(this.id) + && ignored.types.includes(type) + )) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + (this as any)._addEventListener(type, listener, useCapture); + } else if (is.dev()) { + console.log(`Ignoring event: "${this.id}.${type}()"`); + } + }; +} + +export default () => { + overrideAddEventListener(); + // Restore original function after finished loading to avoid keeping Element.prototype altered + window.addEventListener('load', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access + Element.prototype.addEventListener = (Element.prototype as any)._addEventListener; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access + (Element.prototype as any)._addEventListener = undefined; + + }, { once: true }); +}; diff --git a/plugins/quality-changer/back.js b/plugins/quality-changer/back.ts similarity index 55% rename from plugins/quality-changer/back.js rename to plugins/quality-changer/back.ts index eafce7a2..957320dd 100644 --- a/plugins/quality-changer/back.js +++ b/plugins/quality-changer/back.ts @@ -1,7 +1,7 @@ -const { ipcMain, dialog } = require('electron'); +import { ipcMain, dialog } from 'electron'; -module.exports = () => { - ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => await dialog.showMessageBox({ +export default () => { + ipcMain.handle('qualityChanger', async (_, qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox({ type: 'question', buttons: qualityLabels, defaultId: currentIndex, diff --git a/plugins/quality-changer/front.js b/plugins/quality-changer/front.ts similarity index 55% rename from plugins/quality-changer/front.js rename to plugins/quality-changer/front.ts index 0ee25ce8..dd47a87b 100644 --- a/plugins/quality-changer/front.js +++ b/plugins/quality-changer/front.ts @@ -1,8 +1,9 @@ -const { ipcRenderer } = require('electron'); +import { ipcRenderer } from 'electron'; -const { ElementFromFile, templatePath } = require('../utils'); +import { ElementFromFile, templatePath } from '../utils'; +import { YoutubePlayer } from '../../types/youtube-player'; -function $(selector) { +function $(selector: string): HTMLElement | null { return document.querySelector(selector); } @@ -10,32 +11,19 @@ const qualitySettingsButton = ElementFromFile( templatePath(__dirname, 'qualitySettingsTemplate.html'), ); -module.exports = () => { - document.addEventListener('apiLoaded', setup, { once: true, passive: true }); -}; - -function setup(event) { - /** - * @type {{ - * getAvailableQualityLevels: () => string[], - * getPlaybackQuality: () => string, - * getAvailableQualityLabels: () => string[], - * setPlaybackQualityRange: (quality: string) => void, - * setPlaybackQuality: (quality: string) => void, - * }} - */ +function setup(event: CustomEvent) { const api = event.detail; - $('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton); + $('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton); qualitySettingsButton.addEventListener('click', function chooseQuality() { - setTimeout(() => $('#player').click()); + setTimeout(() => $('#player')?.click()); const qualityLevels = api.getAvailableQualityLevels(); const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality()); - ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise) => { + ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise: { response: number }) => { if (promise.response === -1) { return; } @@ -46,3 +34,7 @@ function setup(event) { }); }); } + +export default () => { + document.addEventListener('apiLoaded', setup, { once: true, passive: true }); +}; diff --git a/plugins/shortcuts/back.js b/plugins/shortcuts/back.ts similarity index 54% rename from plugins/shortcuts/back.js rename to plugins/shortcuts/back.ts index ca4500a4..8447092f 100644 --- a/plugins/shortcuts/back.js +++ b/plugins/shortcuts/back.ts @@ -1,24 +1,26 @@ -const { globalShortcut } = require('electron'); -const is = require('electron-is'); -const electronLocalshortcut = require('electron-localshortcut'); +import { BrowserWindow, globalShortcut } from 'electron'; +import is from 'electron-is'; +import electronLocalshortcut from 'electron-localshortcut'; -const registerMPRIS = require('./mpris'); +import registerMPRIS from './mpris'; -const getSongControls = require('../../providers/song-controls'); +import getSongControls from '../../providers/song-controls'; -function _registerGlobalShortcut(webContents, shortcut, action) { +import type { ConfigType } from '../../config/dynamic'; + +function _registerGlobalShortcut(webContents: Electron.WebContents, shortcut: string, action: (webContents: Electron.WebContents) => void) { globalShortcut.register(shortcut, () => { action(webContents); }); } -function _registerLocalShortcut(win, shortcut, action) { +function _registerLocalShortcut(win: BrowserWindow, shortcut: string, action: (webContents: Electron.WebContents) => void) { electronLocalshortcut.register(win, shortcut, () => { action(win.webContents); }); } -function registerShortcuts(win, options) { +function registerShortcuts(win: BrowserWindow, options: ConfigType<'shortcuts'>) { const songControls = getSongControls(win); const { playPause, next, previous, search } = songControls; @@ -39,28 +41,29 @@ function registerShortcuts(win, options) { const shortcutOptions = { global, local }; for (const optionType in shortcutOptions) { - registerAllShortcuts(shortcutOptions[optionType], optionType); + registerAllShortcuts(shortcutOptions[optionType as 'global' | 'local'], optionType); } - function registerAllShortcuts(container, type) { + function registerAllShortcuts(container: Record, type: string) { for (const action in container) { if (!container[action]) { continue; // Action accelerator is empty } console.debug(`Registering ${type} shortcut`, container[action], ':', action); - if (!songControls[action]) { + const actionCallback: () => void = songControls[action as keyof typeof songControls]; + if (typeof actionCallback !== 'function') { console.warn('Invalid action', action); continue; } if (type === 'global') { - _registerGlobalShortcut(win.webContents, container[action], songControls[action]); + _registerGlobalShortcut(win.webContents, container[action], actionCallback); } else { // Type === "local" - _registerLocalShortcut(win, local[action], songControls[action]); + _registerLocalShortcut(win, local[action], actionCallback); } } } } -module.exports = registerShortcuts; +export default registerShortcuts; diff --git a/plugins/shortcuts/menu.js b/plugins/shortcuts/menu.ts similarity index 56% rename from plugins/shortcuts/menu.js rename to plugins/shortcuts/menu.ts index 66f6a566..b5bc2471 100644 --- a/plugins/shortcuts/menu.js +++ b/plugins/shortcuts/menu.ts @@ -1,9 +1,14 @@ -const prompt = require('custom-electron-prompt'); +import prompt, { KeybindOptions } from 'custom-electron-prompt'; -const { setMenuOptions } = require('../../config/plugins'); -const promptOptions = require('../../providers/prompt-options'); +import { BrowserWindow } from 'electron'; -module.exports = (win, options) => [ +import { setMenuOptions } from '../../config/plugins'; +import type { ConfigType } from '../../config/dynamic'; + +import promptOptions from '../../providers/prompt-options'; +import { MenuTemplate } from '../../menu'; + +export default (win: BrowserWindow, options: ConfigType<'shortcuts'>): MenuTemplate => [ { label: 'Set Global Song Controls', click: () => promptKeybind(options, win), @@ -16,7 +21,11 @@ module.exports = (win, options) => [ }, ]; -function setOption(options, key = null, newValue = null) { +function setOption = keyof ConfigType<'shortcuts'>>( + options: ConfigType<'shortcuts'>, + key: Key | null = null, + newValue: ConfigType<'shortcuts'>[Key] | null = null, +) { if (key && newValue !== null) { options[key] = newValue; } @@ -25,9 +34,9 @@ function setOption(options, key = null, newValue = null) { } // Helper function for keybind prompt -const kb = (label_, value_, default_) => ({ value: value_, label: label_, default: default_ }); +const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ value: value_, label: label_, default: default_ }); -async function promptKeybind(options, win) { +async function promptKeybind(options: ConfigType<'shortcuts'>, win: BrowserWindow) { const output = await prompt({ title: 'Global Keybinds', label: 'Choose Global Keybinds for Songs Control:', diff --git a/plugins/shortcuts/mpris-service.d.ts b/plugins/shortcuts/mpris-service.d.ts new file mode 100644 index 00000000..3f9ceff8 --- /dev/null +++ b/plugins/shortcuts/mpris-service.d.ts @@ -0,0 +1,129 @@ +declare module 'mpris-service' { + import { EventEmitter } from 'events'; + + import dbus from 'dbus-next'; + + + interface RootInterfaceOptions { + identity: string; + supportedUriSchemes: string[]; + supportedMimeTypes: string[]; + desktopEntry: string; + } + + export interface Track { + 'mpris:trackid'?: string; + 'mpris:length'?: number; + 'mpris:artUrl'?: string; + 'xesam:album'?: string; + 'xesam:albumArtist'?: string[]; + 'xesam:artist'?: string[]; + 'xesam:asText'?: string; + 'xesam:audioBPM'?: number; + 'xesam:autoRating'?: number; + 'xesam:comment'?: string[]; + 'xesam:composer'?: string[]; + 'xesam:contentCreated'?: string; + 'xesam:discNumber'?: number; + 'xesam:firstUsed'?: string; + 'xesam:genre'?: string[]; + 'xesam:lastUsed'?: string; + 'xesam:lyricist'?: string[]; + 'xesam:title'?: string; + 'xesam:trackNumber'?: number; + 'xesam:url'?: string; + 'xesam:useCount'?: number; + 'xesam:userRating'?: number; + } + + declare class Player extends EventEmitter { + constructor(opts: { + name: string; + identity: string; + supportedMimeTypes?: string[]; + supportedInterfaces?: string[]; + }); + + name: string; + identity: string; + fullscreen: boolean; + supportedUriSchemes: string[]; + supportedMimeTypes: string[]; + canQuit: boolean; + canRaise: boolean; + canSetFullscreen: boolean; + hasTrackList: boolean; + desktopEntry: string; + playbackStatus: string; + loopStatus: string; + shuffle: boolean; + metadata: object; + volume: number; + canControl: boolean; + canPause: boolean; + canPlay: boolean; + canSeek: boolean; + canGoNext: boolean; + canGoPrevious: boolean; + rate: number; + minimumRate: number; + maximumRate: number; + playlists: unknown[]; + activePlaylist: string; + + init(opts: RootInterfaceOptions): void; + + objectPath(subpath?: string): string; + + seeked(position: number): void; + + getTrackIndex(trackId: string): number; + + getTrack(trackId: string): Track; + + addTrack(track: Track): void; + + removeTrack(trackId: string): void; + + getPlaylistIndex(playlistId: string): number; + + setPlaylists(playlists: Track[]): void; + + setActivePlaylist(playlistId: string): void; + + static PLAYBACK_STATUS_PLAYING: 'Playing'; + static PLAYBACK_STATUS_PAUSED: 'Paused'; + static PLAYBACK_STATUS_STOPPED: 'Stopped'; + static LOOP_STATUS_NONE: 'None'; + static LOOP_STATUS_TRACK: 'Track'; + static LOOP_STATUS_PLAYLIST: 'Playlist'; + } + + interface MprisInterface extends dbus.interface.Interface { + setProperty(property: string, valuePlain: unknown): void; + } + + interface RootInterface { + } + + interface PlayerInterface { + } + + interface TracklistInterface { + + TrackListReplaced(tracks: Track[]): void; + + TrackAdded(afterTrack: string): void; + + TrackRemoved(trackId: string): void; + } + + interface PlaylistsInterface { + + PlaylistChanged(playlist: unknown[]): void; + + setActivePlaylistId(playlistId: string): void; + } + + export default Player; +} diff --git a/plugins/shortcuts/mpris.js b/plugins/shortcuts/mpris.ts similarity index 76% rename from plugins/shortcuts/mpris.js rename to plugins/shortcuts/mpris.ts index 9fe2e4e6..5c7d3dad 100644 --- a/plugins/shortcuts/mpris.js +++ b/plugins/shortcuts/mpris.ts @@ -1,32 +1,34 @@ -const { ipcMain } = require('electron'); -const mpris = require('mpris-service'); +import { BrowserWindow, ipcMain } from 'electron'; -const registerCallback = require('../../providers/song-info'); -const getSongControls = require('../../providers/song-controls'); -const config = require('../../config'); +import mpris, { Track } from 'mpris-service'; + +import registerCallback from '../../providers/song-info'; +import getSongControls from '../../providers/song-controls'; +import config from '../../config'; function setupMPRIS() { - return mpris({ + const instance = new mpris({ name: 'youtube-music', identity: 'YouTube Music', - canRaise: true, - supportedUriSchemes: ['https'], supportedMimeTypes: ['audio/mpeg'], supportedInterfaces: ['player'], - desktopEntry: 'youtube-music', }); + instance.canRaise = true; + instance.supportedUriSchemes = ['https']; + instance.desktopEntry = 'youtube-music'; + return instance; } -/** @param {Electron.BrowserWindow} win */ -function registerMPRIS(win) { +function registerMPRIS(win: BrowserWindow) { const songControls = getSongControls(win); const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls; try { - const secToMicro = (n) => Math.round(Number(n) * 1e6); - const microToSec = (n) => Math.round(Number(n) / 1e6); + // TODO: "Typing" for this arguments + const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6); + const microToSec = (n: unknown) => Math.round(Number(n) / 1e6); - const seekTo = (e) => win.webContents.send('seekTo', microToSec(e.position)); - const seekBy = (o) => win.webContents.send('seekBy', microToSec(o)); + const seekTo = (e: { position: unknown }) => win.webContents.send('seekTo', microToSec(e.position)); + const seekBy = (o: unknown) => win.webContents.send('seekBy', microToSec(o)); const player = setupMPRIS(); @@ -37,12 +39,12 @@ function registerMPRIS(win) { win.webContents.send('setupVolumeChangedListener', 'mpris'); }); - ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t))); + ipcMain.on('seeked', (_, t: number) => player.seeked(secToMicro(t))); let currentSeconds = 0; - ipcMain.on('timeChanged', (_, t) => currentSeconds = t); + ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t); - ipcMain.on('repeatChanged', (_, mode) => { + ipcMain.on('repeatChanged', (_, mode: string) => { switch (mode) { case 'NONE': { player.loopStatus = mpris.LOOP_STATUS_NONE; @@ -64,7 +66,7 @@ function registerMPRIS(win) { } } }); - player.on('loopStatus', (status) => { + player.on('loopStatus', (status: string) => { // SwitchRepeat cycles between states in that order const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK]; const currentIndex = switches.indexOf(player.loopStatus); @@ -75,8 +77,6 @@ function registerMPRIS(win) { songControls.switchRepeat(delta); }); - player.getPosition = () => secToMicro(currentSeconds); - player.on('raise', () => { win.setSkipTaskbar(false); win.show(); @@ -114,7 +114,7 @@ function registerMPRIS(win) { let mprisVolNewer = false; let autoUpdate = false; ipcMain.on('volumeChanged', (_, newVol) => { - if (Number.parseInt(player.volume * 100) !== newVol) { + if (~~(player.volume * 100) !== newVol) { if (mprisVolNewer) { mprisVolNewer = false; autoUpdate = false; @@ -130,8 +130,8 @@ function registerMPRIS(win) { player.on('volume', (newVolume) => { if (config.plugins.isEnabled('precise-volume')) { // With precise volume we can set the volume to the exact value. - const newVol = Number.parseInt(newVolume * 100); - if (Number.parseInt(player.volume * 100) !== newVol && !autoUpdate) { + const newVol = ~~(newVolume * 100); + if (~~(player.volume * 100) !== newVol && !autoUpdate) { mprisVolNewer = true; autoUpdate = false; win.webContents.send('setVolume', newVol); @@ -155,9 +155,9 @@ function registerMPRIS(win) { registerCallback((songInfo) => { if (player) { - const data = { + const data: Track = { 'mpris:length': secToMicro(songInfo.songDuration), - 'mpris:artUrl': songInfo.imageSrc, + 'mpris:artUrl': songInfo.imageSrc ?? undefined, 'xesam:title': songInfo.title, 'xesam:url': songInfo.url, 'xesam:artist': [songInfo.artist], @@ -177,4 +177,4 @@ function registerMPRIS(win) { } } -module.exports = registerMPRIS; +export default registerMPRIS; diff --git a/plugins/skip-silences/front.ts b/plugins/skip-silences/front.ts index 6ad8853a..b1db4199 100644 --- a/plugins/skip-silences/front.ts +++ b/plugins/skip-silences/front.ts @@ -1,7 +1,6 @@ -import config from '../../config'; +import type { ConfigType } from '../../config/dynamic'; -const SkipSilencesOptionsObj = config.get('plugins.skip-silences'); -type SkipSilencesOptions = typeof SkipSilencesOptionsObj; +type SkipSilencesOptions = ConfigType<'skip-silences'>; export default (options: SkipSilencesOptions) => { let isSilent = false; diff --git a/plugins/sponsorblock/back.js b/plugins/sponsorblock/back.ts similarity index 53% rename from plugins/sponsorblock/back.js rename to plugins/sponsorblock/back.ts index 2014705f..1a95c776 100644 --- a/plugins/sponsorblock/back.js +++ b/plugins/sponsorblock/back.ts @@ -1,26 +1,31 @@ -const { ipcMain } = require('electron'); -const is = require('electron-is'); +import { BrowserWindow, ipcMain } from 'electron'; +import is from 'electron-is'; -const { sortSegments } = require('./segments'); +import { sortSegments } from './segments'; -const defaultConfig = require('../../config/defaults'); +import { SkipSegment } from './types'; -let videoID; +import defaultConfig from '../../config/defaults'; +import { GetPlayerResponse } from '../../types/get-player-response'; -module.exports = (win, options) => { +import type { ConfigType } from '../../config/dynamic'; + +let videoID: string; + +export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => { const { apiURL, categories } = { ...defaultConfig.plugins.sponsorblock, ...options, }; - ipcMain.on('video-src-changed', async (_, data) => { - videoID = JSON.parse(data)?.videoDetails?.videoId; + ipcMain.on('video-src-changed', async (_, data: string) => { + videoID = (JSON.parse(data) as GetPlayerResponse)?.videoDetails?.videoId; const segments = await fetchSegments(apiURL, categories); win.webContents.send('sponsorblock-skip', segments); }); }; -const fetchSegments = async (apiURL, categories) => { +const fetchSegments = async (apiURL: string, categories: string[]) => { const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify( categories, )}`; @@ -36,7 +41,7 @@ const fetchSegments = async (apiURL, categories) => { return []; } - const segments = await resp.json(); + const segments = await resp.json() as SkipSegment[]; return sortSegments( segments.map((submission) => submission.segment), ); diff --git a/plugins/sponsorblock/front.js b/plugins/sponsorblock/front.js deleted file mode 100644 index d045f36b..00000000 --- a/plugins/sponsorblock/front.js +++ /dev/null @@ -1,30 +0,0 @@ -const { ipcRenderer } = require('electron'); -const is = require('electron-is'); - -let currentSegments = []; - -module.exports = () => { - ipcRenderer.on('sponsorblock-skip', (_, segments) => { - currentSegments = segments; - }); - - document.addEventListener('apiLoaded', () => { - const video = document.querySelector('video'); - - video.addEventListener('timeupdate', (e) => { - for (const segment of currentSegments) { - if ( - e.target.currentTime >= segment[0] - && e.target.currentTime < segment[1] - ) { - e.target.currentTime = segment[1]; - if (is.dev()) { - console.log('SponsorBlock: skipping segment', segment); - } - } - } - }); - // Reset segments on song end - video.addEventListener('emptied', () => currentSegments = []); - }, { once: true, passive: true }); -}; diff --git a/plugins/sponsorblock/front.ts b/plugins/sponsorblock/front.ts new file mode 100644 index 00000000..960c97ba --- /dev/null +++ b/plugins/sponsorblock/front.ts @@ -0,0 +1,35 @@ +import { ipcRenderer } from 'electron'; +import is from 'electron-is'; + +import { Segment } from './types'; + +let currentSegments: Segment[] = []; + +export default () => { + ipcRenderer.on('sponsorblock-skip', (_, segments: Segment[]) => { + currentSegments = segments; + }); + + document.addEventListener('apiLoaded', () => { + const video = document.querySelector('video') as HTMLVideoElement | undefined; + if (!video) return; + + video.addEventListener('timeupdate', (e) => { + const target = e.target as HTMLVideoElement; + + for (const segment of currentSegments) { + if ( + target.currentTime >= segment[0] + && target.currentTime < segment[1] + ) { + target.currentTime = segment[1]; + if (is.dev()) { + console.log('SponsorBlock: skipping segment', segment); + } + } + } + }); + // Reset segments on song end + video.addEventListener('emptied', () => currentSegments = []); + }, { once: true, passive: true }); +}; diff --git a/plugins/sponsorblock/segments.js b/plugins/sponsorblock/segments.ts similarity index 69% rename from plugins/sponsorblock/segments.js rename to plugins/sponsorblock/segments.ts index bc034878..e6d38666 100644 --- a/plugins/sponsorblock/segments.js +++ b/plugins/sponsorblock/segments.ts @@ -1,13 +1,15 @@ // Segments are an array [ [start, end], … ] -module.exports.sortSegments = (segments) => { +import { Segment } from './types'; + +export const sortSegments = (segments: Segment[]) => { segments.sort((segment1, segment2) => segment1[0] === segment2[0] ? segment1[1] - segment2[1] : segment1[0] - segment2[0], ); - const compiledSegments = []; - let currentSegment; + const compiledSegments: Segment[] = []; + let currentSegment: Segment | undefined; for (const segment of segments) { if (!currentSegment) { @@ -24,7 +26,9 @@ module.exports.sortSegments = (segments) => { currentSegment[1] = Math.max(currentSegment[1], segment[1]); } - compiledSegments.push(currentSegment); + if (currentSegment) { + compiledSegments.push(currentSegment); + } return compiledSegments; }; diff --git a/plugins/sponsorblock/types.ts b/plugins/sponsorblock/types.ts new file mode 100644 index 00000000..4bfe7e36 --- /dev/null +++ b/plugins/sponsorblock/types.ts @@ -0,0 +1,12 @@ +export type Segment = [number, number]; + +export interface SkipSegment { // Array of this object + segment: Segment; //[0, 15.23] start and end time in seconds + UUID: string, + category: string, // [1] + videoDuration: number // Duration of video when submission occurred (to be used to determine when a submission is out of date). 0 when unknown. +- 1 second + actionType: string, // [3] + locked: number, // if submission is locked + votes: number, // Votes on segment + description: string, // title for chapters, empty string for other segments +} diff --git a/plugins/touchbar/back.ts b/plugins/touchbar/back.ts index 01091b33..bbfea578 100644 --- a/plugins/touchbar/back.ts +++ b/plugins/touchbar/back.ts @@ -61,7 +61,7 @@ const touchBar = new TouchBar({ ], }); -module.exports = (win: BrowserWindow) => { +export default (win: BrowserWindow) => { const { playPause, next, previous, dislike, like } = getSongControls(win); // If the page is ready, register the callback diff --git a/plugins/tuna-obs/back.ts b/plugins/tuna-obs/back.ts index 76c26997..c01b6557 100644 --- a/plugins/tuna-obs/back.ts +++ b/plugins/tuna-obs/back.ts @@ -44,7 +44,7 @@ const post = (data: Data) => { }).catch((error: { code: number, errno: number }) => console.log(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`)); }; -module.exports = (win: BrowserWindow) => { +export default (win: BrowserWindow) => { ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener')); ipcMain.on('timeChanged', (_, t: number) => { if (!data.title) { diff --git a/plugins/utils.ts b/plugins/utils.ts index e5bfdc63..441af4f8 100644 --- a/plugins/utils.ts +++ b/plugins/utils.ts @@ -29,7 +29,7 @@ export const triggerAction = (channel: string, act export const triggerActionSync = (channel: string, action: ValueOf, ...args: Parameters): unknown => ipcRenderer.sendSync(channel, action, ...args); -export const listenAction = (channel: string, callback: (event: Electron.IpcMainEvent, ...args: Parameters) => void) => ipcMain.on(channel, callback); +export const listenAction = (channel: string, callback: (event: Electron.IpcMainEvent, action: string) => void) => ipcMain.on(channel, callback); export const fileExists = ( path: fs.PathLike, @@ -48,7 +48,7 @@ export const fileExists = ( }; const cssToInject = new Map(); -export const injectCSS = (webContents: Electron.WebContents, filepath: unknown, cb = undefined) => { +export const injectCSS = (webContents: Electron.WebContents, filepath: unknown, cb: (() => void) | undefined = undefined) => { if (cssToInject.size === 0) { setupCssInjection(webContents); } diff --git a/plugins/video-toggle/back.js b/plugins/video-toggle/back.js deleted file mode 100644 index d6d0968f..00000000 --- a/plugins/video-toggle/back.js +++ /dev/null @@ -1,11 +0,0 @@ -const path = require('node:path'); - -const { injectCSS } = require('../utils'); - -module.exports = (win, options) => { - if (options.forceHide) { - injectCSS(win.webContents, path.join(__dirname, 'force-hide.css')); - } else if (!options.mode || options.mode === 'custom') { - injectCSS(win.webContents, path.join(__dirname, 'button-switcher.css')); - } -}; diff --git a/plugins/video-toggle/back.ts b/plugins/video-toggle/back.ts new file mode 100644 index 00000000..944900e8 --- /dev/null +++ b/plugins/video-toggle/back.ts @@ -0,0 +1,14 @@ +import path from 'node:path'; + +import { BrowserWindow } from 'electron'; + +import { injectCSS } from '../utils'; +import type { ConfigType } from '../../config/dynamic'; + +export default (win: BrowserWindow, options: ConfigType<'video-toggle'>) => { + if (options.forceHide) { + injectCSS(win.webContents, path.join(__dirname, 'force-hide.css')); + } else if (!options.mode || options.mode === 'custom') { + injectCSS(win.webContents, path.join(__dirname, 'button-switcher.css')); + } +}; diff --git a/plugins/video-toggle/front.js b/plugins/video-toggle/front.ts similarity index 52% rename from plugins/video-toggle/front.js rename to plugins/video-toggle/front.ts index c36d0015..839b899f 100644 --- a/plugins/video-toggle/front.js +++ b/plugins/video-toggle/front.ts @@ -1,37 +1,41 @@ -const { ElementFromFile, templatePath } = require('../utils'); -const { setOptions, isEnabled } = require('../../config/plugins'); +import { ElementFromFile, templatePath } from '../utils'; +import { setOptions, isEnabled } from '../../config/plugins'; -const moveVolumeHud = isEnabled('precise-volume') ? require('../precise-volume/front').moveVolumeHud : () => { -}; +import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '../precise-volume/front'; +import type { ConfigType } from '../../config/dynamic'; +import { YoutubePlayer } from '../../types/youtube-player'; +import { ThumbnailElement } from '../../types/get-player-response'; -function $(selector) { +const moveVolumeHud = isEnabled('precise-volume') ? preciseVolumeMoveVolumeHud : () => {}; + +function $(selector: string): HTMLElement | null { return document.querySelector(selector); } -let options; -let player; -let video; -let api; +let options: ConfigType<'video-toggle'>; +let player: HTMLElement & { videoMode_: boolean }; +let video: HTMLVideoElement; +let api: YoutubePlayer; const switchButtonDiv = ElementFromFile( templatePath(__dirname, 'button_template.html'), ); -module.exports = (_options) => { +export default (_options: ConfigType<'video-toggle'>) => { if (_options.forceHide) { return; } switch (_options.mode) { case 'native': { - $('ytmusic-player-page').setAttribute('has-av-switcher'); - $('ytmusic-player').setAttribute('has-av-switcher'); + $('ytmusic-player-page')?.setAttribute('has-av-switcher', ''); + $('ytmusic-player')?.setAttribute('has-av-switcher', ''); return; } case 'disabled': { - $('ytmusic-player-page').removeAttribute('has-av-switcher'); - $('ytmusic-player').removeAttribute('has-av-switcher'); + $('ytmusic-player-page')?.removeAttribute('has-av-switcher'); + $('ytmusic-player')?.removeAttribute('has-av-switcher'); return; } @@ -43,15 +47,15 @@ module.exports = (_options) => { } }; -function setup(e) { +function setup(e: CustomEvent) { api = e.detail; - player = $('ytmusic-player'); - video = $('video'); + player = $('ytmusic-player') as typeof player; + video = $('video') as HTMLVideoElement; - $('#main-panel').append(switchButtonDiv); + ($('#main-panel') as HTMLVideoElement).append(switchButtonDiv); if (options.hideVideo) { - $('.video-switch-button-checkbox').checked = false; + ($('.video-switch-button-checkbox') as HTMLInputElement).checked = false; changeDisplay(false); forcePlaybackMode(); // Fix black video @@ -60,8 +64,9 @@ function setup(e) { // Button checked = show video switchButtonDiv.addEventListener('change', (e) => { - options.hideVideo = !e.target.checked; - changeDisplay(e.target.checked); + const target = e.target as HTMLInputElement; + options.hideVideo = target.checked; + changeDisplay(target.checked); setOptions('video-toggle', options); }); @@ -87,12 +92,12 @@ function setup(e) { } } -function changeDisplay(showVideo) { +function changeDisplay(showVideo: boolean) { player.style.margin = showVideo ? '' : 'auto 0px'; player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED'); - $('#song-video.ytmusic-player').style.display = showVideo ? 'block' : 'none'; - $('#song-image').style.display = showVideo ? 'none' : 'block'; + $('#song-video.ytmusic-player')!.style.display = showVideo ? 'block' : 'none'; + $('#song-image')!.style.display = showVideo ? 'none' : 'block'; if (showVideo && !video.style.top) { video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`; @@ -108,12 +113,12 @@ function videoStarted() { // Hide toggle button switchButtonDiv.style.display = 'none'; } else { - // Switch to high res thumbnail - forceThumbnail($('#song-image img')); + // Switch to high-res thumbnail + forceThumbnail($('#song-image img') as HTMLImageElement); // Show toggle button switchButtonDiv.style.display = 'initial'; // Change display to video mode if video exist & video is hidden & option.hideVideo = false - if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === 'none') { + if (!options.hideVideo && $('#song-video.ytmusic-player')?.style.display === 'none') { changeDisplay(true); } else { moveVolumeHud(!options.hideVideo); @@ -126,9 +131,10 @@ function videoStarted() { function forcePlaybackMode() { const playbackModeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { - if (mutation.target.getAttribute('playback-mode') !== 'ATV_PREFERRED') { + const target = mutation.target as HTMLElement; + if (target.getAttribute('playback-mode') !== 'ATV_PREFERRED') { playbackModeObserver.disconnect(); - mutation.target.setAttribute('playback-mode', 'ATV_PREFERRED'); + target.setAttribute('playback-mode', 'ATV_PREFERRED'); } } }); @@ -142,19 +148,21 @@ function observeThumbnail() { } for (const mutation of mutations) { - if (!mutation.target.src.startsWith('data:')) { + const target = mutation.target as HTMLImageElement; + if (!target.src.startsWith('data:')) { continue; } - forceThumbnail(mutation.target); + forceThumbnail(target); } }); - playbackModeObserver.observe($('#song-image img'), { attributeFilter: ['src'] }); + playbackModeObserver.observe($('#song-image img')!, { attributeFilter: ['src'] }); } -function forceThumbnail(img) { - const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails; +function forceThumbnail(img: HTMLImageElement) { + const thumbnails: ThumbnailElement[] = ($('#movie_player') as unknown as YoutubePlayer).getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? []; if (thumbnails && thumbnails.length > 0) { - img.src = thumbnails.at(-1).url.split('?')[0]; + const thumbnail = thumbnails.at(-1)?.url.split('?')[0]; + if (typeof thumbnail === 'string') img.src = thumbnail; } } diff --git a/plugins/video-toggle/menu.js b/plugins/video-toggle/menu.ts similarity index 85% rename from plugins/video-toggle/menu.js rename to plugins/video-toggle/menu.ts index 76703796..8afac05e 100644 --- a/plugins/video-toggle/menu.js +++ b/plugins/video-toggle/menu.ts @@ -1,6 +1,10 @@ -const { setMenuOptions } = require('../../config/plugins'); +import { BrowserWindow } from 'electron'; -module.exports = (win, options) => [ +import { setMenuOptions } from '../../config/plugins'; +import type { ConfigType } from '../../config/dynamic'; +import { MenuTemplate } from '../../menu'; + +export default (win: BrowserWindow, options: ConfigType<'video-toggle'>): MenuTemplate => [ { label: 'Mode', submenu: [ diff --git a/plugins/visualizer/back.js b/plugins/visualizer/back.js deleted file mode 100644 index 7a884474..00000000 --- a/plugins/visualizer/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, 'empty-player.css')); -}; diff --git a/plugins/visualizer/back.ts b/plugins/visualizer/back.ts new file mode 100644 index 00000000..a66e9ded --- /dev/null +++ b/plugins/visualizer/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, 'empty-player.css')); +}; diff --git a/plugins/visualizer/front.js b/plugins/visualizer/front.ts similarity index 62% rename from plugins/visualizer/front.js rename to plugins/visualizer/front.ts index 67194dea..591fcef8 100644 --- a/plugins/visualizer/front.js +++ b/plugins/visualizer/front.ts @@ -1,19 +1,28 @@ -const defaultConfig = require('../../config/defaults'); +import { Visualizer } from './visualizers/visualizer'; -module.exports = (options) => { +import vudio from './visualizers/vudio'; +import wave from './visualizers/wave'; + +import type { ConfigType } from '../../config/dynamic'; +import defaultConfig from '../../config/defaults'; + +export default (options: ConfigType<'visualizer'>) => { const optionsWithDefaults = { ...defaultConfig.plugins.visualizer, ...options, }; - const VisualizerType = require(`./visualizers/${optionsWithDefaults.type}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let visualizerType: { new(...args: any[]): Visualizer } = vudio; + if (optionsWithDefaults.type === 'wave') visualizerType = wave; document.addEventListener( 'audioCanPlay', (e) => { - const video = document.querySelector('video'); - const visualizerContainer = document.querySelector('#player'); + const video = document.querySelector('video') as (HTMLVideoElement & { captureStream(): MediaStream; }); + const visualizerContainer = document.querySelector('#player') as HTMLElement; - let canvas = document.querySelector('#visualizer'); + let canvas = document.querySelector('#visualizer') as HTMLCanvasElement; if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'visualizer'; @@ -33,17 +42,17 @@ module.exports = (options) => { gainNode.gain.value = 1.25; e.detail.audioSource.connect(gainNode); - const visualizer = new VisualizerType( + const visualizer = new visualizerType( e.detail.audioContext, e.detail.audioSource, visualizerContainer, canvas, gainNode, video.captureStream(), - optionsWithDefaults[optionsWithDefaults.type], + optionsWithDefaults, ); - const resizeVisualizer = (width, height) => { + const resizeVisualizer = (width: number, height: number) => { resizeCanvas(); visualizer.resize(width, height); }; diff --git a/plugins/visualizer/menu.js b/plugins/visualizer/menu.ts similarity index 54% rename from plugins/visualizer/menu.js rename to plugins/visualizer/menu.ts index 310332f3..681e0527 100644 --- a/plugins/visualizer/menu.js +++ b/plugins/visualizer/menu.ts @@ -1,13 +1,19 @@ -const { readdirSync } = require('node:fs'); -const path = require('node:path'); +import { readdirSync } from 'node:fs'; +import path from 'node:path'; -const { setMenuOptions } = require('../../config/plugins'); +import { BrowserWindow } from 'electron'; + +import { setMenuOptions } from '../../config/plugins'; + +import { MenuTemplate } from '../../menu'; + +import type { ConfigType } from '../../config/dynamic'; const visualizerTypes = readdirSync(path.join(__dirname, 'visualizers')).map( (filename) => path.parse(filename).name, ); -module.exports = (win, options) => [ +export default (win: BrowserWindow, options: ConfigType<'visualizer'>): MenuTemplate => [ { label: 'Type', submenu: visualizerTypes.map((visualizerType) => ({ diff --git a/plugins/visualizer/visualizers/butterchurn.js b/plugins/visualizer/visualizers/butterchurn.js deleted file mode 100644 index 2c8b5a1f..00000000 --- a/plugins/visualizer/visualizers/butterchurn.js +++ /dev/null @@ -1,47 +0,0 @@ -const butterchurn = require('butterchurn'); -const butterchurnPresets = require('butterchurn-presets'); - -const presets = butterchurnPresets.getPresets(); - -class ButterchurnVisualizer { - constructor( - audioContext, - audioSource, - visualizerContainer, - canvas, - audioNode, - stream, - options, - ) { - this.visualizer = butterchurn.default.createVisualizer( - audioContext, - canvas, - { - width: canvas.width, - height: canvas.height, - }, - ); - - const preset = presets[options.preset]; - this.visualizer.loadPreset(preset, options.blendTimeInSeconds); - - this.visualizer.connectAudio(audioNode); - - this.renderingFrequencyInMs = options.renderingFrequencyInMs; - } - - resize(width, height) { - this.visualizer.setRendererSize(width, height); - } - - render() { - const renderVisualizer = () => { - requestAnimationFrame(() => renderVisualizer()); - this.visualizer.render(); - }; - - setTimeout(renderVisualizer(), this.renderingFrequencyInMs); - } -} - -module.exports = ButterchurnVisualizer; diff --git a/plugins/visualizer/visualizers/visualizer.ts b/plugins/visualizer/visualizers/visualizer.ts new file mode 100644 index 00000000..38555ca4 --- /dev/null +++ b/plugins/visualizer/visualizers/visualizer.ts @@ -0,0 +1,18 @@ +import type { ConfigType } from '../../../config/dynamic'; + +export abstract class Visualizer { + abstract visualizer: T; + + protected constructor( + audioContext: AudioContext, + audioSource: MediaElementAudioSourceNode, + visualizerContainer: HTMLElement, + canvas: HTMLCanvasElement, + audioNode: GainNode, + stream: MediaStream, + options: ConfigType<'visualizer'>, + ) {} + + abstract resize(width: number, height: number): void; + abstract render(): void; +} diff --git a/plugins/visualizer/visualizers/vudio.js b/plugins/visualizer/visualizers/vudio.js deleted file mode 100644 index c2a04aca..00000000 --- a/plugins/visualizer/visualizers/vudio.js +++ /dev/null @@ -1,33 +0,0 @@ -const Vudio = require('vudio/umd/vudio'); - -class VudioVisualizer { - constructor( - audioContext, - audioSource, - visualizerContainer, - canvas, - audioNode, - stream, - options, - ) { - this.visualizer = new Vudio(stream, canvas, { - width: canvas.width, - height: canvas.height, - // Visualizer config - ...options, - }); - } - - resize(width, height) { - this.visualizer.setOption({ - width, - height, - }); - } - - render() { - this.visualizer.dance(); - } -} - -module.exports = VudioVisualizer; diff --git a/plugins/visualizer/visualizers/vudio.ts b/plugins/visualizer/visualizers/vudio.ts new file mode 100644 index 00000000..052bae98 --- /dev/null +++ b/plugins/visualizer/visualizers/vudio.ts @@ -0,0 +1,49 @@ +import Vudio from 'vudio'; + +import { Visualizer } from './visualizer'; + +import type { ConfigType } from '../../../config/dynamic'; + +class VudioVisualizer extends Visualizer { + visualizer: Vudio; + + constructor( + audioContext: AudioContext, + audioSource: MediaElementAudioSourceNode, + visualizerContainer: HTMLElement, + canvas: HTMLCanvasElement, + audioNode: GainNode, + stream: MediaStream, + options: ConfigType<'visualizer'>, + ) { + super( + audioContext, + audioSource, + visualizerContainer, + canvas, + audioNode, + stream, + options, + ); + + this.visualizer = new Vudio(stream, canvas, { + width: canvas.width, + height: canvas.height, + // Visualizer config + ...options, + }); + } + + resize(width: number, height: number) { + this.visualizer.setOptions({ + width, + height, + }); + } + + render() { + this.visualizer.dance(); + } +} + +export default VudioVisualizer; diff --git a/plugins/visualizer/visualizers/wave.js b/plugins/visualizer/visualizers/wave.js deleted file mode 100644 index f594c785..00000000 --- a/plugins/visualizer/visualizers/wave.js +++ /dev/null @@ -1,34 +0,0 @@ -const { Wave } = require('@foobar404/wave'); - -class WaveVisualizer { - constructor( - audioContext, - audioSource, - visualizerContainer, - canvas, - audioNode, - stream, - options, - ) { - this.visualizer = new Wave( - { context: audioContext, source: audioSource }, - canvas, - ); - for (const animation of options.animations) { - this.visualizer.addAnimation( - eval(`new this.visualizer.animations.${animation.type}( - ${JSON.stringify(animation.config)} - )`), - ); - } - } - - // eslint-disable-next-line no-unused-vars - resize(width, height) { - } - - render() { - } -} - -module.exports = WaveVisualizer; diff --git a/plugins/visualizer/visualizers/wave.ts b/plugins/visualizer/visualizers/wave.ts new file mode 100644 index 00000000..21a4005e --- /dev/null +++ b/plugins/visualizer/visualizers/wave.ts @@ -0,0 +1,49 @@ +import { Wave } from '@foobar404/wave'; + +import { Visualizer } from './visualizer'; + +import type { ConfigType } from '../../../config/dynamic'; + +class WaveVisualizer extends Visualizer { + visualizer: Wave; + + constructor( + audioContext: AudioContext, + audioSource: MediaElementAudioSourceNode, + visualizerContainer: HTMLElement, + canvas: HTMLCanvasElement, + audioNode: GainNode, + stream: MediaStream, + options: ConfigType<'visualizer'>, + ) { + super( + audioContext, + audioSource, + visualizerContainer, + canvas, + audioNode, + stream, + options, + ); + + this.visualizer = new Wave( + { context: audioContext, source: audioSource }, + canvas, + ); + for (const animation of options.wave.animations) { + const TargetVisualizer = this.visualizer.animations[animation.type as keyof typeof this.visualizer.animations]; + + this.visualizer.addAnimation( + new TargetVisualizer(animation.config as never), // Magic of Typescript + ); + } + } + + resize(_: number, __: number) { + } + + render() { + } +} + +export default WaveVisualizer; diff --git a/plugins/visualizer/vudio.d.ts b/plugins/visualizer/vudio.d.ts new file mode 100644 index 00000000..6e5b3b1f --- /dev/null +++ b/plugins/visualizer/vudio.d.ts @@ -0,0 +1,34 @@ +declare module 'vudio' { + interface NoneWaveformOptions { + maxHeight?: number; + minHeight?: number; + spacing?: number; + color?: string | string[]; + shadowBlur?: number; + shadowColor?: string; + fadeSide?: boolean; + } + + interface WaveformOptions extends NoneWaveformOptions{ + horizontalAlign: 'left' | 'center' | 'right'; + verticalAlign: 'top' | 'middle' | 'bottom'; + } + + interface VudioOptions { + effect?: 'waveform' | 'circlewave' | 'circlebar' | 'lighting'; + accuracy?: number; + width?: number; + height?: number; + waveform?: WaveformOptions + } + + class Vudio { + constructor(audio: HTMLAudioElement | MediaStream, canvas: HTMLCanvasElement, options: VudioOptions = {}); + + dance(): void; + pause(): void; + setOptions(options: VudioOptions): void; + } + + export default Vudio; +} diff --git a/types/youtube-player.ts b/types/youtube-player.ts index 51ee4c1f..f19a7db9 100644 --- a/types/youtube-player.ts +++ b/types/youtube-player.ts @@ -43,7 +43,7 @@ export interface YoutubePlayer { getVideoAspectRatio: (...params: Parameters) => Return; getPreferredQuality: (...params: Parameters) => Return; getPlaybackQualityLabel: (...params: Parameters) => Return; - setPlaybackQualityRange: (...params: Parameters) => Return; + setPlaybackQualityRange: (quality: string) => void; onAdUxClicked: (...params: Parameters) => Return; getFeedbackProductData: (...params: Parameters) => Return; getStoryboardFrame: (...params: Parameters) => Return; @@ -51,7 +51,7 @@ export interface YoutubePlayer { getStoryboardLevel: (...params: Parameters) => Return; getNumberOfStoryboardLevels: (...params: Parameters) => Return; getCaptionWindowContainerId: (...params: Parameters) => Return; - getAvailableQualityLabels: (...params: Parameters) => Return; + getAvailableQualityLabels: () => string[]; addUtcCueRange: (...params: Parameters) => Return; showAirplayPicker: (...params: Parameters) => Return; dispatchReduxAction: (...params: Parameters) => Return; @@ -147,14 +147,14 @@ export interface YoutubePlayer { unMute: (...params: Parameters) => Return; isMuted: (...params: Parameters) => Return; setVolume: (...params: Parameters) => Return; - getVolume: (...params: Parameters) => Return; + getVolume: () => number; seekTo: (seconds: number) => void; getPlayerMode: (...params: Parameters) => Return; getPlayerState: (...params: Parameters) => Return; getAvailablePlaybackRates: (...params: Parameters) => Return; - getPlaybackQuality: (...params: Parameters) => Return; - setPlaybackQuality: (...params: Parameters) => Return; - getAvailableQualityLevels: (...params: Parameters) => Return; + getPlaybackQuality: () => string; + setPlaybackQuality: (quality: string) => void; + getAvailableQualityLevels: () => string[]; getCurrentTime: (...params: Parameters) => Return; getDuration: (...params: Parameters) => Return; addEventListener: (...params: Parameters) => Return;