mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
feat: migration to TypeScript FINAL
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -83,6 +83,16 @@ const defaultConfig = {
|
||||
'shortcuts': {
|
||||
enabled: false,
|
||||
overrideMediaKeys: false,
|
||||
global: {
|
||||
previous: '',
|
||||
playPause: '',
|
||||
next: '',
|
||||
} as Record<string, string>,
|
||||
local: {
|
||||
previous: '',
|
||||
playPause: '',
|
||||
next: '',
|
||||
} as Record<string, string>,
|
||||
},
|
||||
'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,
|
||||
|
||||
63
custom-electron-prompt.d.ts
vendored
63
custom-electron-prompt.d.ts
vendored
@ -1,19 +1,29 @@
|
||||
declare module 'custom-electron-prompt' {
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
export interface PromptCounterOptions {
|
||||
export type SelectOptions = Record<string, string>;
|
||||
|
||||
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<HTMLInputElement>;
|
||||
selectOptions?: SelectOptions;
|
||||
}
|
||||
|
||||
interface BasePromptOptions<T extends string> {
|
||||
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<string, string>;
|
||||
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<HTMLInputElement>;
|
||||
multiInputOptions?: {
|
||||
label: string;
|
||||
value: unknown;
|
||||
inputAttrs?: Partial<HTMLInputElement>;
|
||||
selectOptions?: Record<string, string>;
|
||||
}[];
|
||||
}
|
||||
|
||||
const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise<string | null>;
|
||||
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 string> = (
|
||||
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 string> = 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: <T extends Type>(options?: PromptOptions<T> & { type: T }, parent?: BrowserWindow) => Promise<PromptResult<T> | null>;
|
||||
|
||||
export default prompt;
|
||||
}
|
||||
|
||||
7
menu.ts
7
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);
|
||||
},
|
||||
|
||||
250
package-lock.json
generated
250
package-lock.json
generated
@ -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",
|
||||
|
||||
18
package.json
18
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",
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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')) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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'));
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,7 +0,0 @@
|
||||
const path = require('node:path');
|
||||
|
||||
const { injectCSS } = require('../utils');
|
||||
|
||||
module.exports = (win) => {
|
||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||
};
|
||||
9
plugins/no-google-login/back.ts
Normal file
9
plugins/no-google-login/back.ts
Normal file
@ -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'));
|
||||
};
|
||||
@ -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;
|
||||
@ -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();
|
||||
};
|
||||
51
plugins/notifications/back.ts
Normal file
51
plugins/notifications/back.ts
Normal file
@ -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();
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
const { PluginConfig } = require('../../config/dynamic');
|
||||
|
||||
const config = new PluginConfig('notifications');
|
||||
|
||||
module.exports = { ...config };
|
||||
5
plugins/notifications/config.ts
Normal file
5
plugins/notifications/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('notifications');
|
||||
|
||||
export default { ...config } as PluginConfig<'notifications'>;
|
||||
@ -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<typeof getSongControls>;
|
||||
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) =>
|
||||
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||
|
||||
const getButtons = (isPaused) => `\
|
||||
const getButtons = (isPaused: boolean) => `\
|
||||
<actions>
|
||||
${getButton('previous')}
|
||||
${isPaused ? getButton('play') : getButton('pause')}
|
||||
@ -177,7 +178,7 @@ const getButtons = (isPaused) => `\
|
||||
</actions>\
|
||||
`;
|
||||
|
||||
const toast = (content, isPaused) => `\
|
||||
const toast = (content: string, isPaused: boolean) => `\
|
||||
<toast>
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
@ -189,19 +190,19 @@ const toast = (content, isPaused) => `\
|
||||
${getButtons(isPaused)}
|
||||
</toast>`;
|
||||
|
||||
const xmlImage = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
|
||||
const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, 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(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
@ -211,17 +212,17 @@ const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\
|
||||
</subgroup>
|
||||
${xmlMoreData(songInfo)}
|
||||
</group>\
|
||||
`, songInfo.isPaused);
|
||||
`, songInfo.isPaused ?? false);
|
||||
|
||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }) => `\
|
||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
|
||||
<subgroup hint-textStacking="bottom">
|
||||
${album
|
||||
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)}</text>
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text>
|
||||
</subgroup>\
|
||||
`;
|
||||
|
||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
@ -230,9 +231,9 @@ const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`
|
||||
</subgroup>
|
||||
</group>
|
||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||
`, isPaused);
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
||||
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
@ -241,9 +242,9 @@ const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>\
|
||||
`, isPaused);
|
||||
`, isPaused ?? false);
|
||||
|
||||
const titleFontPicker = (title) => {
|
||||
const titleFontPicker = (title: string) => {
|
||||
if (title.length <= 13) {
|
||||
return 'Header';
|
||||
}
|
||||
@ -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
|
||||
@ -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}`;
|
||||
@ -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<PiPOptions>;
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
12
plugins/picture-in-picture/keyboardevent-from-electron-accelerator.d.ts
vendored
Normal file
12
plugins/picture-in-picture/keyboardevent-from-electron-accelerator.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
14
plugins/picture-in-picture/keyboardevents-areequal.d.ts
vendored
Normal file
14
plugins/picture-in-picture/keyboardevents-areequal.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
@ -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;
|
||||
@ -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',
|
||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`);
|
||||
} 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',
|
||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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<ConfigType<'precise-volume'>>, options: ConfigType<'precise-volume'>, win: BrowserWindow) {
|
||||
for (const option in changedOptions) {
|
||||
options[option] = changedOptions[option];
|
||||
// HACK: Weird TypeScript error
|
||||
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[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);
|
||||
@ -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 });
|
||||
};
|
||||
41
plugins/precise-volume/preload.ts
Normal file
41
plugins/precise-volume/preload.ts
Normal file
@ -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 });
|
||||
};
|
||||
@ -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,
|
||||
@ -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<YoutubePlayer>) {
|
||||
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 });
|
||||
};
|
||||
@ -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<string, string>, 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;
|
||||
@ -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<Key extends keyof ConfigType<'shortcuts'> = 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:',
|
||||
129
plugins/shortcuts/mpris-service.d.ts
vendored
Normal file
129
plugins/shortcuts/mpris-service.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
);
|
||||
@ -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 });
|
||||
};
|
||||
35
plugins/sponsorblock/front.ts
Normal file
35
plugins/sponsorblock/front.ts
Normal file
@ -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 });
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
12
plugins/sponsorblock/types.ts
Normal file
12
plugins/sponsorblock/types.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -29,7 +29,7 @@ export const triggerAction = <Parameters extends unknown[]>(channel: string, act
|
||||
|
||||
export const triggerActionSync = <Parameters extends unknown[]>(channel: string, action: ValueOf<typeof Actions>, ...args: Parameters): unknown => ipcRenderer.sendSync(channel, action, ...args);
|
||||
|
||||
export const listenAction = (channel: string, callback: <Parameters extends unknown[]>(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);
|
||||
}
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
};
|
||||
14
plugins/video-toggle/back.ts
Normal file
14
plugins/video-toggle/back.ts
Normal file
@ -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'));
|
||||
}
|
||||
};
|
||||
@ -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<YoutubePlayer>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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: [
|
||||
@ -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'));
|
||||
};
|
||||
9
plugins/visualizer/back.ts
Normal file
9
plugins/visualizer/back.ts
Normal file
@ -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'));
|
||||
};
|
||||
@ -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<unknown> } = 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);
|
||||
};
|
||||
@ -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) => ({
|
||||
@ -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;
|
||||
18
plugins/visualizer/visualizers/visualizer.ts
Normal file
18
plugins/visualizer/visualizers/visualizer.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ConfigType } from '../../../config/dynamic';
|
||||
|
||||
export abstract class Visualizer<T> {
|
||||
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;
|
||||
}
|
||||
@ -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;
|
||||
49
plugins/visualizer/visualizers/vudio.ts
Normal file
49
plugins/visualizer/visualizers/vudio.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import Vudio from 'vudio';
|
||||
|
||||
import { Visualizer } from './visualizer';
|
||||
|
||||
import type { ConfigType } from '../../../config/dynamic';
|
||||
|
||||
class VudioVisualizer extends Visualizer<Vudio> {
|
||||
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;
|
||||
@ -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;
|
||||
49
plugins/visualizer/visualizers/wave.ts
Normal file
49
plugins/visualizer/visualizers/wave.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Wave } from '@foobar404/wave';
|
||||
|
||||
import { Visualizer } from './visualizer';
|
||||
|
||||
import type { ConfigType } from '../../../config/dynamic';
|
||||
|
||||
class WaveVisualizer extends Visualizer<Wave> {
|
||||
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;
|
||||
34
plugins/visualizer/vudio.d.ts
vendored
Normal file
34
plugins/visualizer/vudio.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
@ -43,7 +43,7 @@ export interface YoutubePlayer {
|
||||
getVideoAspectRatio: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getPreferredQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getPlaybackQualityLabel: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
setPlaybackQualityRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
setPlaybackQualityRange: (quality: string) => void;
|
||||
onAdUxClicked: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getFeedbackProductData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getStoryboardFrame: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
@ -51,7 +51,7 @@ export interface YoutubePlayer {
|
||||
getStoryboardLevel: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getNumberOfStoryboardLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getCaptionWindowContainerId: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getAvailableQualityLabels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getAvailableQualityLabels: () => string[];
|
||||
addUtcCueRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
showAirplayPicker: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
dispatchReduxAction: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
@ -147,14 +147,14 @@ export interface YoutubePlayer {
|
||||
unMute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
setVolume: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getVolume: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getVolume: () => number;
|
||||
seekTo: (seconds: number) => void;
|
||||
getPlayerMode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getPlayerState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getAvailablePlaybackRates: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
setPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getAvailableQualityLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getPlaybackQuality: () => string;
|
||||
setPlaybackQuality: (quality: string) => void;
|
||||
getAvailableQualityLevels: () => string[];
|
||||
getCurrentTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
getDuration: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
addEventListener: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||
|
||||
Reference in New Issue
Block a user