mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +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': {
|
'shortcuts': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
overrideMediaKeys: false,
|
overrideMediaKeys: false,
|
||||||
|
global: {
|
||||||
|
previous: '',
|
||||||
|
playPause: '',
|
||||||
|
next: '',
|
||||||
|
} as Record<string, string>,
|
||||||
|
local: {
|
||||||
|
previous: '',
|
||||||
|
playPause: '',
|
||||||
|
next: '',
|
||||||
|
} as Record<string, string>,
|
||||||
},
|
},
|
||||||
'downloader': {
|
'downloader': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -130,7 +140,7 @@ const defaultConfig = {
|
|||||||
volumeUp: '',
|
volumeUp: '',
|
||||||
volumeDown: '',
|
volumeDown: '',
|
||||||
},
|
},
|
||||||
savedVolume: undefined, // Plugin save volume between session here
|
savedVolume: undefined as number | undefined, // Plugin save volume between session here
|
||||||
},
|
},
|
||||||
'sponsorblock': {
|
'sponsorblock': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -146,8 +156,10 @@ const defaultConfig = {
|
|||||||
},
|
},
|
||||||
'video-toggle': {
|
'video-toggle': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
hideVideo: false,
|
||||||
mode: 'custom',
|
mode: 'custom',
|
||||||
forceHide: false,
|
forceHide: false,
|
||||||
|
align: '',
|
||||||
},
|
},
|
||||||
'picture-in-picture': {
|
'picture-in-picture': {
|
||||||
'enabled': false,
|
'enabled': false,
|
||||||
@ -158,6 +170,7 @@ const defaultConfig = {
|
|||||||
'pip-position': [10, 10],
|
'pip-position': [10, 10],
|
||||||
'pip-size': [450, 275],
|
'pip-size': [450, 275],
|
||||||
'isInPiP': false,
|
'isInPiP': false,
|
||||||
|
'useNativePiP': false,
|
||||||
},
|
},
|
||||||
'captions-selector': {
|
'captions-selector': {
|
||||||
enabled: false,
|
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' {
|
declare module 'custom-electron-prompt' {
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
export interface PromptCounterOptions {
|
export type SelectOptions = Record<string, string>;
|
||||||
|
|
||||||
|
export interface CounterOptions {
|
||||||
minimum?: number;
|
minimum?: number;
|
||||||
maximum?: number;
|
maximum?: number;
|
||||||
multiFire?: boolean;
|
multiFire?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptKeybindOptions {
|
export interface KeybindOptions {
|
||||||
value: string;
|
value: string;
|
||||||
label: 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;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizable?: boolean;
|
resizable?: boolean;
|
||||||
@ -25,10 +35,6 @@ declare module 'custom-electron-prompt' {
|
|||||||
};
|
};
|
||||||
alwaysOnTop?: boolean;
|
alwaysOnTop?: boolean;
|
||||||
value?: unknown;
|
value?: unknown;
|
||||||
type?: 'input' | 'select' | 'counter' | 'multiInput';
|
|
||||||
selectOptions?: Record<string, string>;
|
|
||||||
keybindOptions?: PromptKeybindOptions[];
|
|
||||||
counterOptions?: PromptCounterOptions;
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
useHtmlLabel?: boolean;
|
useHtmlLabel?: boolean;
|
||||||
customStylesheet?: string;
|
customStylesheet?: string;
|
||||||
@ -38,15 +44,42 @@ declare module 'custom-electron-prompt' {
|
|||||||
customScript?: string;
|
customScript?: string;
|
||||||
enableRemoteModule?: boolean;
|
enableRemoteModule?: boolean;
|
||||||
inputAttrs?: Partial<HTMLInputElement>;
|
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;
|
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[];
|
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 {
|
return {
|
||||||
label: pluginLabel,
|
label: pluginLabel,
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -274,7 +275,7 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
|||||||
{
|
{
|
||||||
label: 'Proxy',
|
label: 'Proxy',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: Boolean(config.get('options.proxy')),
|
checked: !!(config.get('options.proxy')),
|
||||||
click(item) {
|
click(item) {
|
||||||
setProxy(item, win);
|
setProxy(item, win);
|
||||||
},
|
},
|
||||||
|
|||||||
250
package-lock.json
generated
250
package-lock.json
generated
@ -52,6 +52,7 @@
|
|||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
|
"copyfiles": "2.4.1",
|
||||||
"del-cli": "5.0.1",
|
"del-cli": "5.0.1",
|
||||||
"electron": "27.0.0-alpha.5",
|
"electron": "27.0.0-alpha.5",
|
||||||
"electron-builder": "24.6.3",
|
"electron-builder": "24.6.3",
|
||||||
@ -2834,6 +2835,76 @@
|
|||||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/core-js": {
|
||||||
"version": "2.6.12",
|
"version": "2.6.12",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||||
@ -3162,6 +3233,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@ -3543,6 +3629,21 @@
|
|||||||
"unzip-crx-3": "^0.2.0"
|
"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": {
|
"node_modules/electron-is": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-is/-/electron-is-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-is/-/electron-is-3.0.0.tgz",
|
||||||
@ -4507,6 +4608,21 @@
|
|||||||
"node": ">=12.0.0"
|
"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": {
|
"node_modules/flatted": {
|
||||||
"version": "3.2.7",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
||||||
@ -6596,6 +6712,21 @@
|
|||||||
"node": "^12.13 || ^14.13 || >=16"
|
"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": {
|
"node_modules/node-id3": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
||||||
@ -6615,6 +6746,40 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/nopt": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
||||||
@ -7617,21 +7782,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/roarr": {
|
||||||
"version": "2.15.4",
|
"version": "2.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
"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": {
|
"node_modules/titleize": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
|
||||||
@ -8522,6 +8718,21 @@
|
|||||||
"tmp": "^0.2.0"
|
"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": {
|
"node_modules/tmp-promise/node_modules/tmp": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||||
@ -9079,6 +9290,15 @@
|
|||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
18
package.json
18
package.json
@ -84,21 +84,22 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:debug": "DEBUG=pw:browser* 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",
|
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron ./dist/index.js",
|
||||||
"generate:package": "node utils/generate-package-json.js",
|
"generate:package": "node utils/generate-package-json.js",
|
||||||
"postinstall": "npm run plugins",
|
"postinstall": "npm run plugins",
|
||||||
"clean": "del-cli dist && del-cli pack",
|
"clean": "del-cli dist && del-cli pack",
|
||||||
"build": "npm run clean && tsc && electron-builder --win --mac --linux -p never",
|
"copy-files": "copyfiles -u 1 plugins/**/*.html plugins/**/*.css plugins/**/*.bin plugins/**/*.js dist/plugins/",
|
||||||
"build:linux": "npm run clean && electron-builder --linux -p never",
|
"build": "npm run clean && tsc && npm run copy-files && electron-builder --win --mac --linux -p never",
|
||||||
"build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never",
|
"build:linux": "npm run clean && tsc && npm run copy-files && electron-builder --linux -p never",
|
||||||
"build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never",
|
"build:mac": "npm run clean && tsc && npm run copy-files && electron-builder --mac dmg:x64 -p never",
|
||||||
"build:win": "npm run clean && electron-builder --win -p never",
|
"build:mac:arm64": "npm run clean && tsc && npm run copy-files && electron-builder --mac dmg:arm64 -p never",
|
||||||
"build:win:x64": "npm run clean && electron-builder --win nsis:x64 -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",
|
"lint": "xo",
|
||||||
"changelog": "auto-changelog",
|
"changelog": "auto-changelog",
|
||||||
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
|
"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",
|
"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:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "npm run clean && electron-builder --mac -p always",
|
"release:mac": "npm run clean && electron-builder --mac -p always",
|
||||||
@ -157,6 +158,7 @@
|
|||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
|
"copyfiles": "2.4.1",
|
||||||
"del-cli": "5.0.1",
|
"del-cli": "5.0.1",
|
||||||
"electron": "27.0.0-alpha.5",
|
"electron": "27.0.0-alpha.5",
|
||||||
"electron-builder": "24.6.3",
|
"electron-builder": "24.6.3",
|
||||||
|
|||||||
@ -3,10 +3,9 @@ import { BrowserWindow } from 'electron';
|
|||||||
import { loadAdBlockerEngine } from './blocker';
|
import { loadAdBlockerEngine } from './blocker';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
import pluginConfig from '../../config';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
const AdBlockOptionsObj = pluginConfig.get('plugins.adblocker');
|
type AdBlockOptions = ConfigType<'adblocker'>;
|
||||||
type AdBlockOptions = typeof AdBlockOptionsObj;
|
|
||||||
|
|
||||||
export default async (win: BrowserWindow, options: AdBlockOptions) => {
|
export default async (win: BrowserWindow, options: AdBlockOptions) => {
|
||||||
if (await config.shouldUseBlocklists()) {
|
if (await config.shouldUseBlocklists()) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import configProvider from './config';
|
|||||||
|
|
||||||
import { ElementFromFile, templatePath } from '../utils';
|
import { ElementFromFile, templatePath } from '../utils';
|
||||||
import { YoutubePlayer } from '../../types/youtube-player';
|
import { YoutubePlayer } from '../../types/youtube-player';
|
||||||
import { ConfigType } from '../../config/dynamic';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
interface LanguageOptions {
|
interface LanguageOptions {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { VolumeFader } from './fader';
|
|||||||
import configProvider from './config';
|
import configProvider from './config';
|
||||||
|
|
||||||
import defaultConfigs from '../../config/defaults';
|
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 transitionAudio: Howl; // Howler audio used to fade out the current music
|
||||||
let firstVideo = true;
|
let firstVideo = true;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import config from './config';
|
|||||||
|
|
||||||
import promptOptions from '../../providers/prompt-options';
|
import promptOptions from '../../providers/prompt-options';
|
||||||
import configOptions from '../../config/defaults';
|
import configOptions from '../../config/defaults';
|
||||||
import { ConfigType } from '../../config/dynamic';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
const defaultOptions = configOptions.plugins.crossfade;
|
const defaultOptions = configOptions.plugins.crossfade;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { dev } from 'electron-is';
|
|||||||
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||||
|
|
||||||
import registerCallback from '../../providers/song-info';
|
import registerCallback from '../../providers/song-info';
|
||||||
import pluginConfig from '../../config';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
// Application ID registered by @Zo-Bro-23
|
// Application ID registered by @Zo-Bro-23
|
||||||
const clientId = '1043858434585526382';
|
const clientId = '1043858434585526382';
|
||||||
@ -118,8 +118,7 @@ export const connect = (showError = false) => {
|
|||||||
let clearActivity: NodeJS.Timeout | undefined;
|
let clearActivity: NodeJS.Timeout | undefined;
|
||||||
let updateActivity: import('../../providers/song-info').SongInfoCallback;
|
let updateActivity: import('../../providers/song-info').SongInfoCallback;
|
||||||
|
|
||||||
const DiscordOptionsObj = pluginConfig.get('plugins.discord');
|
type DiscordOptions = ConfigType<'discord'>;
|
||||||
type DiscordOptions = typeof DiscordOptionsObj;
|
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
win: Electron.BrowserWindow,
|
win: Electron.BrowserWindow,
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import { clear, connect, isConnected, registerRefresh } from './back';
|
|||||||
import { setMenuOptions } from '../../config/plugins';
|
import { setMenuOptions } from '../../config/plugins';
|
||||||
import promptOptions from '../../providers/prompt-options';
|
import promptOptions from '../../providers/prompt-options';
|
||||||
import { singleton } from '../../providers/decorators';
|
import { singleton } from '../../providers/decorators';
|
||||||
import config from '../../config';
|
|
||||||
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||||
registerRefresh(refreshMenu);
|
registerRefresh(refreshMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
const DiscordOptionsObj = config.get('plugins.discord');
|
type DiscordOptions = ConfigType<'discord'>;
|
||||||
type DiscordOptions = typeof DiscordOptionsObj;
|
|
||||||
|
|
||||||
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => {
|
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => {
|
||||||
registerRefreshOnce(refreshMenu);
|
registerRefreshOnce(refreshMenu);
|
||||||
|
|||||||
@ -411,7 +411,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
|
|
||||||
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
|
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
|
||||||
|
|
||||||
const folder = getFolder(config.get('downloadFolder'));
|
const folder = getFolder(config.get('downloadFolder') ?? '');
|
||||||
const playlistFolder = join(folder, safePlaylistTitle);
|
const playlistFolder = join(folder, safePlaylistTitle);
|
||||||
if (existsSync(playlistFolder)) {
|
if (existsSync(playlistFolder)) {
|
||||||
if (!config.get('skipExisting')) {
|
if (!config.get('skipExisting')) {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default (): MenuTemplate => [
|
|||||||
click() {
|
click() {
|
||||||
const result = dialog.showOpenDialogSync({
|
const result = dialog.showOpenDialogSync({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
defaultPath: getFolder(config.get('downloadFolder')),
|
defaultPath: getFolder(config.get('downloadFolder') ?? ''),
|
||||||
});
|
});
|
||||||
if (result) {
|
if (result) {
|
||||||
config.set('downloadFolder', result[0]);
|
config.set('downloadFolder', result[0]);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ setupTitlebar();
|
|||||||
|
|
||||||
// Tracks menu visibility
|
// Tracks menu visibility
|
||||||
|
|
||||||
module.exports = (win: BrowserWindow) => {
|
export default (win: BrowserWindow) => {
|
||||||
// Css for custom scrollbar + disable drag area(was causing bugs)
|
// Css for custom scrollbar + disable drag area(was causing bugs)
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,8 @@ function $(selector: string) {
|
|||||||
return document.querySelector(selector);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
export default () => {
|
||||||
const visible = () => Boolean($('.cet-menubar')?.firstChild);
|
const visible = () => !!($('.cet-menubar')?.firstChild);
|
||||||
const bar = new Titlebar({
|
const bar = new Titlebar({
|
||||||
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
|
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
|
||||||
backgroundColor: Color.fromHex('#050505'),
|
backgroundColor: Color.fromHex('#050505'),
|
||||||
|
|||||||
@ -4,10 +4,9 @@ import md5 from 'md5';
|
|||||||
import { setOptions } from '../../config/plugins';
|
import { setOptions } from '../../config/plugins';
|
||||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||||
import defaultConfig from '../../config/defaults';
|
import defaultConfig from '../../config/defaults';
|
||||||
import config from '../../config';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
const LastFMOptionsObj = config.get('plugins.last-fm');
|
type LastFMOptions = ConfigType<'last-fm'>;
|
||||||
type LastFMOptions = typeof LastFMOptionsObj;
|
|
||||||
|
|
||||||
interface LastFmData {
|
interface LastFmData {
|
||||||
method: string,
|
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 { cleanupName, SongInfo } from '../../providers/song-info';
|
||||||
|
|
||||||
import { injectCSS } from '../utils';
|
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;
|
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||||
let revRomanized = false;
|
let revRomanized = false;
|
||||||
|
|
||||||
const LyricGeniusTypeObj = config.get('plugins.lyric-genius');
|
export type LyricGeniusType = ConfigType<'lyric-genius'>;
|
||||||
export type LyricGeniusType = typeof LyricGeniusTypeObj;
|
|
||||||
|
|
||||||
export default (win: BrowserWindow, options: LyricGeniusType) => {
|
export default (win: BrowserWindow, options: LyricGeniusType) => {
|
||||||
if (options.romanizedLyrics) {
|
if (options.romanizedLyrics) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { LyricGeniusType, toggleRomanized } from './back';
|
|||||||
|
|
||||||
import { setOptions } from '../../config/plugins';
|
import { setOptions } from '../../config/plugins';
|
||||||
|
|
||||||
module.exports = (win: BrowserWindow, options: LyricGeniusType) => [
|
export default (_: BrowserWindow, options: LyricGeniusType) => [
|
||||||
{
|
{
|
||||||
label: 'Romanized Lyrics',
|
label: 'Romanized Lyrics',
|
||||||
type: 'checkbox',
|
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'), () => {
|
injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => {
|
||||||
win.webContents.send('navigation-css-ready');
|
win.webContents.send('navigation-css-ready');
|
||||||
});
|
});
|
||||||
|
|
||||||
listenAction(CHANNEL, (event, action) => {
|
listenAction(CHANNEL, (_, action) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTIONS.NEXT: {
|
case ACTIONS.NEXT: {
|
||||||
if (win.webContents.canGoForward()) {
|
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', () => {
|
ipcRenderer.on('navigation-css-ready', () => {
|
||||||
const forwardButton = ElementFromFile(
|
const forwardButton = ElementFromFile(
|
||||||
templatePath(__dirname, 'forward.html'),
|
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(
|
const menuEntries = document.querySelectorAll(
|
||||||
'#items ytmusic-guide-entry-renderer',
|
'#items ytmusic-guide-entry-renderer',
|
||||||
);
|
);
|
||||||
for (const item of menuEntries) {
|
menuEntries.forEach((item) => {
|
||||||
const icon = item.querySelector('path');
|
const icon = item.querySelector('path');
|
||||||
if (icon) {
|
if (icon) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
@ -26,7 +26,7 @@ function removeLoginElements() {
|
|||||||
item.remove();
|
item.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
childList: true,
|
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';
|
||||||
|
|
||||||
/**
|
import getSongControls from '../../providers/song-controls';
|
||||||
* @type {PluginConfig}
|
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||||
*/
|
import { changeProtocolHandler } from '../../providers/protocol-handler';
|
||||||
const config = require('./config');
|
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
|
||||||
|
|
||||||
const getSongControls = require('../../providers/song-controls');
|
let songControls: ReturnType<typeof getSongControls>;
|
||||||
const registerCallback = require('../../providers/song-info');
|
let savedNotification: Notification | undefined;
|
||||||
const { changeProtocolHandler } = require('../../providers/protocol-handler');
|
|
||||||
const { setTrayOnClick, setTrayOnDoubleClick } = require('../../tray');
|
|
||||||
|
|
||||||
|
export default (win: BrowserWindow) => {
|
||||||
let songControls;
|
|
||||||
let savedNotification;
|
|
||||||
|
|
||||||
/** @param {Electron.BrowserWindow} win */
|
|
||||||
module.exports = (win) => {
|
|
||||||
songControls = getSongControls(win);
|
songControls = getSongControls(win);
|
||||||
|
|
||||||
let currentSeconds = 0;
|
let currentSeconds = 0;
|
||||||
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||||
|
|
||||||
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
saveTempIcon();
|
saveTempIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
let savedSongInfo;
|
let savedSongInfo: SongInfo;
|
||||||
let lastUrl;
|
let lastUrl: string | undefined;
|
||||||
|
|
||||||
// Register songInfoCallback
|
// Register songInfoCallback
|
||||||
registerCallback((songInfo) => {
|
registerCallback((songInfo) => {
|
||||||
@ -78,7 +72,7 @@ module.exports = (win) => {
|
|||||||
changeProtocolHandler(
|
changeProtocolHandler(
|
||||||
(cmd) => {
|
(cmd) => {
|
||||||
if (Object.keys(songControls).includes(cmd)) {
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
songControls[cmd]();
|
songControls[cmd as keyof typeof songControls]();
|
||||||
if (config.get('refreshOnPlayPause') && (
|
if (config.get('refreshOnPlayPause') && (
|
||||||
cmd === 'pause'
|
cmd === 'pause'
|
||||||
|| (cmd === 'play' && !config.get('unpauseNotification'))
|
|| (cmd === 'play' && !config.get('unpauseNotification'))
|
||||||
@ -97,11 +91,18 @@ module.exports = (win) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendNotification(songInfo) {
|
function sendNotification(songInfo: SongInfo) {
|
||||||
const iconSrc = notificationImage(songInfo);
|
const iconSrc = notificationImage(songInfo);
|
||||||
|
|
||||||
savedNotification?.close();
|
savedNotification?.close();
|
||||||
|
|
||||||
|
let icon: string;
|
||||||
|
if (typeof iconSrc === 'object') {
|
||||||
|
icon = iconSrc.toDataURL();
|
||||||
|
} else {
|
||||||
|
icon = iconSrc;
|
||||||
|
}
|
||||||
|
|
||||||
savedNotification = new Notification({
|
savedNotification = new Notification({
|
||||||
title: songInfo.title || 'Playing',
|
title: songInfo.title || 'Playing',
|
||||||
body: songInfo.artist,
|
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/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/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
|
||||||
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
|
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
|
||||||
toastXml: getXml(songInfo, iconSrc),
|
toastXml: getXml(songInfo, icon),
|
||||||
});
|
});
|
||||||
|
|
||||||
savedNotification.on('close', () => {
|
savedNotification.on('close', () => {
|
||||||
@ -121,7 +122,7 @@ function sendNotification(songInfo) {
|
|||||||
savedNotification.show();
|
savedNotification.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
const getXml = (songInfo, iconSrc) => {
|
const getXml = (songInfo: SongInfo, iconSrc: string) => {
|
||||||
switch (config.get('toastStyle')) {
|
switch (config.get('toastStyle')) {
|
||||||
default:
|
default:
|
||||||
case ToastStyles.logo:
|
case ToastStyles.logo:
|
||||||
@ -155,7 +156,7 @@ const iconLocation = app.isPackaged
|
|||||||
? path.resolve(app.getPath('userData'), 'icons')
|
? path.resolve(app.getPath('userData'), 'icons')
|
||||||
: path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
|
: path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
|
||||||
|
|
||||||
const display = (kind) => {
|
const display = (kind: keyof typeof icons) => {
|
||||||
if (config.get('toastStyle') === ToastStyles.legacy) {
|
if (config.get('toastStyle') === ToastStyles.legacy) {
|
||||||
return `content="${icons[kind]}"`;
|
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}"/>`;
|
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||||
|
|
||||||
const getButtons = (isPaused) => `\
|
const getButtons = (isPaused: boolean) => `\
|
||||||
<actions>
|
<actions>
|
||||||
${getButton('previous')}
|
${getButton('previous')}
|
||||||
${isPaused ? getButton('play') : getButton('pause')}
|
${isPaused ? getButton('play') : getButton('pause')}
|
||||||
@ -177,7 +178,7 @@ const getButtons = (isPaused) => `\
|
|||||||
</actions>\
|
</actions>\
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const toast = (content, isPaused) => `\
|
const toast = (content: string, isPaused: boolean) => `\
|
||||||
<toast>
|
<toast>
|
||||||
<audio silent="true" />
|
<audio silent="true" />
|
||||||
<visual>
|
<visual>
|
||||||
@ -189,19 +190,19 @@ const toast = (content, isPaused) => `\
|
|||||||
${getButtons(isPaused)}
|
${getButtons(isPaused)}
|
||||||
</toast>`;
|
</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}/>
|
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||||
<text id="1">${title}</text>
|
<text id="1">${title}</text>
|
||||||
<text id="2">${artist}</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" />
|
<image id="1" src="${imgSrc}" name="Image" />
|
||||||
<text>ㅤ</text>
|
<text>ㅤ</text>
|
||||||
<group>
|
<group>
|
||||||
@ -211,17 +212,17 @@ const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\
|
|||||||
</subgroup>
|
</subgroup>
|
||||||
${xmlMoreData(songInfo)}
|
${xmlMoreData(songInfo)}
|
||||||
</group>\
|
</group>\
|
||||||
`, songInfo.isPaused);
|
`, songInfo.isPaused ?? false);
|
||||||
|
|
||||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }) => `\
|
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
|
||||||
<subgroup hint-textStacking="bottom">
|
<subgroup hint-textStacking="bottom">
|
||||||
${album
|
${album
|
||||||
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
? `<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>\
|
</subgroup>\
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||||
<text>ㅤ</text>
|
<text>ㅤ</text>
|
||||||
<group>
|
<group>
|
||||||
<subgroup hint-weight="1" hint-textStacking="center">
|
<subgroup hint-weight="1" hint-textStacking="center">
|
||||||
@ -230,9 +231,9 @@ const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`
|
|||||||
</subgroup>
|
</subgroup>
|
||||||
</group>
|
</group>
|
||||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
<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" />
|
<image id="1" src="${imgSrc}" name="Image" />
|
||||||
<text>ㅤ</text>
|
<text>ㅤ</text>
|
||||||
<group>
|
<group>
|
||||||
@ -241,9 +242,9 @@ const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
|||||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||||
</subgroup>
|
</subgroup>
|
||||||
</group>\
|
</group>\
|
||||||
`, isPaused);
|
`, isPaused ?? false);
|
||||||
|
|
||||||
const titleFontPicker = (title) => {
|
const titleFontPicker = (title: string) => {
|
||||||
if (title.length <= 13) {
|
if (title.length <= 13) {
|
||||||
return 'Header';
|
return 'Header';
|
||||||
}
|
}
|
||||||
@ -1,9 +1,14 @@
|
|||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
|
|
||||||
const { urgencyLevels, ToastStyles, snakeToCamel } = require('./utils');
|
import {BrowserWindow, MenuItem} from 'electron';
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
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()
|
...(is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -24,7 +29,7 @@ module.exports = (_win, options) => [
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.interactive,
|
checked: options.interactive,
|
||||||
// Doesn't update until restart
|
// 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)
|
// 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',
|
label: 'Open/Close on tray click',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.trayControls,
|
checked: options.trayControls,
|
||||||
click: (item) => config.set('trayControls', item.checked),
|
click: (item: MenuItem) => config.set('trayControls', item.checked),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hide Button Text',
|
label: 'Hide Button Text',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.hideButtonText,
|
checked: options.hideButtonText,
|
||||||
click: (item) => config.set('hideButtonText', item.checked),
|
click: (item: MenuItem) => config.set('hideButtonText', item.checked),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Refresh on Play/Pause',
|
label: 'Refresh on Play/Pause',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.refreshOnPlayPause,
|
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',
|
label: 'Show notification on unpause',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.unpauseNotification,
|
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 });
|
const array = Array.from({ length: Object.keys(ToastStyles).length });
|
||||||
|
|
||||||
// ToastStyles index starts from 1
|
// 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 icon = 'assets/youtube-music.png';
|
||||||
const userData = app.getPath('userData');
|
const userData = app.getPath('userData');
|
||||||
const temporaryIcon = path.join(userData, 'tempIcon.png');
|
const temporaryIcon = path.join(userData, 'tempIcon.png');
|
||||||
const temporaryBanner = path.join(userData, 'tempBanner.png');
|
const temporaryBanner = path.join(userData, 'tempBanner.png');
|
||||||
|
|
||||||
const { cache } = require('../../providers/decorators');
|
|
||||||
|
|
||||||
module.exports.ToastStyles = {
|
export const ToastStyles = {
|
||||||
logo: 1,
|
logo: 1,
|
||||||
banner_centered_top: 2,
|
banner_centered_top: 2,
|
||||||
hero: 3,
|
hero: 3,
|
||||||
@ -23,20 +24,20 @@ module.exports.ToastStyles = {
|
|||||||
legacy: 7,
|
legacy: 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.icons = {
|
export const icons = {
|
||||||
play: '\u{1405}', // ᐅ
|
play: '\u{1405}', // ᐅ
|
||||||
pause: '\u{2016}', // ‖
|
pause: '\u{2016}', // ‖
|
||||||
next: '\u{1433}', // ᐳ
|
next: '\u{1433}', // ᐳ
|
||||||
previous: '\u{1438}', // ᐸ
|
previous: '\u{1438}', // ᐸ
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.urgencyLevels = [
|
export const urgencyLevels = [
|
||||||
{ name: 'Low', value: 'low' },
|
{ name: 'Low', value: 'low' },
|
||||||
{ name: 'Normal', value: 'normal' },
|
{ name: 'Normal', value: 'normal' },
|
||||||
{ name: 'High', value: 'critical' },
|
{ name: 'High', value: 'critical' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const nativeImageToLogo = cache((nativeImage) => {
|
const nativeImageToLogo = cache((nativeImage: NativeImage) => {
|
||||||
const temporaryImage = nativeImage.resize({ height: 256 });
|
const temporaryImage = nativeImage.resize({ height: 256 });
|
||||||
const margin = Math.max(temporaryImage.getSize().width - 256, 0);
|
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) {
|
if (!songInfo.image) {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
@ -58,30 +59,30 @@ module.exports.notificationImage = (songInfo) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (config.get('toastStyle')) {
|
switch (config.get('toastStyle')) {
|
||||||
case module.exports.ToastStyles.logo:
|
case ToastStyles.logo:
|
||||||
case module.exports.ToastStyles.legacy: {
|
case ToastStyles.legacy: {
|
||||||
return this.saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
|
return saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
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 {
|
try {
|
||||||
fs.writeFileSync(savePath, img.toPNG());
|
fs.writeFileSync(savePath, img.toPNG());
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.log(`Error writing song icon to disk:\n${error.toString()}`);
|
console.log(`Error writing song icon to disk:\n${String(error)}`);
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return savePath;
|
return savePath;
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.saveTempIcon = () => {
|
export const saveTempIcon = () => {
|
||||||
for (const kind of Object.keys(module.exports.icons)) {
|
for (const kind of Object.keys(icons)) {
|
||||||
const destinationPath = path.join(userData, 'icons', `${kind}.png`);
|
const destinationPath = path.join(userData, 'icons', `${kind}.png`);
|
||||||
if (fs.existsSync(destinationPath)) {
|
if (fs.existsSync(destinationPath)) {
|
||||||
continue;
|
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()
|
group.toUpperCase()
|
||||||
.replace('-', ' ')
|
.replace('-', ' ')
|
||||||
.replace('_', ' '),
|
.replace('_', ' '),
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports.secondsToMinutes = (seconds) => {
|
export const secondsToMinutes = (seconds: number) => {
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const secondsLeft = seconds % 60;
|
const secondsLeft = seconds % 60;
|
||||||
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
|
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
|
||||||
@ -5,7 +5,7 @@ import { app, BrowserWindow, ipcMain } from 'electron';
|
|||||||
import { setOptions as setPluginOptions } from '../../config/plugins';
|
import { setOptions as setPluginOptions } from '../../config/plugins';
|
||||||
import { injectCSS } from '../utils';
|
import { injectCSS } from '../utils';
|
||||||
|
|
||||||
import config from '../../config';
|
import type { ConfigType } from '../../config/dynamic';
|
||||||
|
|
||||||
let isInPiP = false;
|
let isInPiP = false;
|
||||||
let originalPosition: number[];
|
let originalPosition: number[];
|
||||||
@ -15,9 +15,7 @@ let originalMaximized: boolean;
|
|||||||
|
|
||||||
let win: BrowserWindow;
|
let win: BrowserWindow;
|
||||||
|
|
||||||
// Magic of TypeScript
|
type PiPOptions = ConfigType<'picture-in-picture'>;
|
||||||
const PiPOptionsObj = config.get('plugins.picture-in-picture');
|
|
||||||
type PiPOptions = typeof PiPOptionsObj;
|
|
||||||
|
|
||||||
let options: Partial<PiPOptions>;
|
let options: Partial<PiPOptions>;
|
||||||
|
|
||||||
|
|||||||
@ -2,31 +2,44 @@ import { ipcRenderer } from 'electron';
|
|||||||
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||||
import keyEventAreEqual from 'keyboardevents-areequal';
|
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||||
|
|
||||||
const { getSongMenu } = require('../../providers/dom-elements');
|
import { getSongMenu } from '../../providers/dom-elements';
|
||||||
const { ElementFromFile, templatePath } = require('../utils');
|
|
||||||
|
|
||||||
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);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
let useNativePiP = false;
|
let useNativePiP = false;
|
||||||
let menu = null;
|
let menu: Element | null = null;
|
||||||
const pipButton = ElementFromFile(
|
const pipButton = ElementFromFile(
|
||||||
templatePath(__dirname, 'picture-in-picture.html'),
|
templatePath(__dirname, 'picture-in-picture.html'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Will also clone
|
// Will also clone
|
||||||
function replaceButton(query, button) {
|
function replaceButton(query: string, button: Element) {
|
||||||
const svg = button.querySelector('#icon svg').cloneNode(true);
|
const svg = button.querySelector('#icon svg')?.cloneNode(true);
|
||||||
|
if (svg) {
|
||||||
button.replaceWith(button.cloneNode(true));
|
button.replaceWith(button.cloneNode(true));
|
||||||
button.remove();
|
button.remove();
|
||||||
const newButton = $(query);
|
const newButton = $(query);
|
||||||
newButton.querySelector('#icon').append(svg);
|
if (newButton) {
|
||||||
|
newButton.querySelector('#icon')?.append(svg);
|
||||||
|
}
|
||||||
return newButton;
|
return newButton;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneButton(query) {
|
function cloneButton(query: string) {
|
||||||
replaceButton(query, $(query));
|
const button = $(query);
|
||||||
|
if (button) {
|
||||||
|
replaceButton(query, button);
|
||||||
|
}
|
||||||
return $(query);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuUrl = $(
|
const menuUrl = ($(
|
||||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
||||||
)?.href;
|
) as HTMLAnchorElement)?.href;
|
||||||
if (menuUrl && !menuUrl.includes('watch?')) {
|
if (menuUrl && !menuUrl.includes('watch?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,15 +73,15 @@ const observer = new MutationObserver(() => {
|
|||||||
const togglePictureInPicture = async () => {
|
const togglePictureInPicture = async () => {
|
||||||
if (useNativePiP) {
|
if (useNativePiP) {
|
||||||
const isInPiP = document.pictureInPictureElement !== null;
|
const isInPiP = document.pictureInPictureElement !== null;
|
||||||
const video = $('video');
|
const video = $('video') as HTMLVideoElement | null;
|
||||||
const togglePiP = () =>
|
const togglePiP = () =>
|
||||||
isInPiP
|
isInPiP
|
||||||
? document.exitPictureInPicture.call(document)
|
? document.exitPictureInPicture.call(document)
|
||||||
: video.requestPictureInPicture.call(video);
|
: video?.requestPictureInPicture?.call(video);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await togglePiP();
|
await togglePiP();
|
||||||
$('#icon').click(); // Close the menu
|
($('#icon') as HTMLButtonElement | null)?.click(); // Close the menu
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
@ -72,24 +90,26 @@ const togglePictureInPicture = async () => {
|
|||||||
ipcRenderer.send('picture-in-picture');
|
ipcRenderer.send('picture-in-picture');
|
||||||
return false;
|
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 listenForToggle = () => {
|
||||||
const originalExitButton = $('.exit-fullscreen-button');
|
const originalExitButton = $('.exit-fullscreen-button') as HTMLButtonElement;
|
||||||
const appLayout = $('ytmusic-app-layout');
|
const appLayout = $('ytmusic-app-layout') as HTMLElement;
|
||||||
const expandMenu = $('#expanding-menu');
|
const expandMenu = $('#expanding-menu') as HTMLElement;
|
||||||
const middleControls = $('.middle-controls');
|
const middleControls = $('.middle-controls') as HTMLButtonElement;
|
||||||
const playerPage = $('ytmusic-player-page');
|
const playerPage = $('ytmusic-player-page') as HTMLElement & { playerPageOpen_: boolean };
|
||||||
const togglePlayerPageButton = $('.toggle-player-page-button');
|
const togglePlayerPageButton = $('.toggle-player-page-button') as HTMLButtonElement;
|
||||||
const fullScreenButton = $('.fullscreen-button');
|
const fullScreenButton = $('.fullscreen-button') as HTMLButtonElement;
|
||||||
const player = $('#player');
|
const player = ($('#player') as (HTMLVideoElement & { onDoubleClick_: () => void | undefined }));
|
||||||
const onPlayerDblClick = player.onDoubleClick_;
|
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) {
|
if (isPip) {
|
||||||
replaceButton('.exit-fullscreen-button', originalExitButton).addEventListener('click', () => togglePictureInPicture());
|
replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture());
|
||||||
player.onDoubleClick_ = () => {
|
player.onDoubleClick_ = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,9 +124,9 @@ const listenForToggle = () => {
|
|||||||
titlebar.style.display = 'none';
|
titlebar.style.display = 'none';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$('.exit-fullscreen-button').replaceWith(originalExitButton);
|
$('.exit-fullscreen-button')?.replaceWith(originalExitButton);
|
||||||
player.onDoubleClick_ = onPlayerDblClick;
|
player.onDoubleClick_ = onPlayerDblClick;
|
||||||
expandMenu.onmouseleave = undefined;
|
expandMenu.onmouseleave = null;
|
||||||
originalExitButton.click();
|
originalExitButton.click();
|
||||||
appLayout.classList.remove('pip');
|
appLayout.classList.remove('pip');
|
||||||
if (titlebar) {
|
if (titlebar) {
|
||||||
@ -116,22 +136,23 @@ const listenForToggle = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function observeMenu(options) {
|
function observeMenu(options: PiPOptions) {
|
||||||
useNativePiP = options.useNativePiP;
|
useNativePiP = options.useNativePiP;
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
'apiLoaded',
|
'apiLoaded',
|
||||||
() => {
|
() => {
|
||||||
listenForToggle();
|
listenForToggle();
|
||||||
|
|
||||||
cloneButton('.player-minimize-button').addEventListener('click', async () => {
|
cloneButton('.player-minimize-button')?.addEventListener('click', async () => {
|
||||||
await togglePictureInPicture();
|
await togglePictureInPicture();
|
||||||
setTimeout(() => $('#player').click());
|
setTimeout(() => ($('#player') as HTMLButtonElement | undefined)?.click());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Allows easily closing the menu by programmatically clicking outside of it
|
// 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
|
// 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,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
@ -140,7 +161,7 @@ function observeMenu(options) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (options) => {
|
export default (options: PiPOptions) => {
|
||||||
observeMenu(options);
|
observeMenu(options);
|
||||||
|
|
||||||
if (options.hotkey) {
|
if (options.hotkey) {
|
||||||
@ -148,7 +169,7 @@ module.exports = (options) => {
|
|||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
if (
|
if (
|
||||||
keyEventAreEqual(event, hotkeyEvent)
|
keyEventAreEqual(event, hotkeyEvent)
|
||||||
&& !$('ytmusic-search-box').opened
|
&& !($('ytmusic-search-box') as (HTMLElement & { opened: boolean }) | undefined)?.opened
|
||||||
) {
|
) {
|
||||||
togglePictureInPicture();
|
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',
|
label: 'Always on top',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
@ -33,7 +37,7 @@ module.exports = (win, options) => [
|
|||||||
{
|
{
|
||||||
label: 'Hotkey',
|
label: 'Hotkey',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: options.hotkey,
|
checked: !!options.hotkey,
|
||||||
async click(item) {
|
async click(item) {
|
||||||
const output = await prompt({
|
const output = await prompt({
|
||||||
title: 'Picture in Picture Hotkey',
|
title: 'Picture in Picture Hotkey',
|
||||||
@ -51,7 +55,7 @@ module.exports = (win, options) => [
|
|||||||
const { value, accelerator } = output[0];
|
const { value, accelerator } = output[0];
|
||||||
setOptions({ [value]: accelerator });
|
setOptions({ [value]: accelerator });
|
||||||
|
|
||||||
item.checked = Boolean(accelerator);
|
item.checked = !!accelerator;
|
||||||
} else {
|
} else {
|
||||||
// Reset checkbox if prompt was canceled
|
// Reset checkbox if prompt was canceled
|
||||||
item.checked = !item.checked;
|
item.checked = !item.checked;
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
const { getSongMenu } = require('../../providers/dom-elements');
|
import { getSongMenu } from '../../providers/dom-elements';
|
||||||
const { ElementFromFile, templatePath } = require('../utils');
|
import { ElementFromFile, templatePath } from '../utils';
|
||||||
const { singleton } = require('../../providers/decorators');
|
import { singleton } from '../../providers/decorators';
|
||||||
|
|
||||||
function $(selector) {
|
|
||||||
|
function $(selector: string) {
|
||||||
return document.querySelector(selector);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
const slider = ElementFromFile(templatePath(__dirname, 'slider.html'));
|
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 MIN_PLAYBACK_SPEED = 0.07;
|
||||||
const MAX_PLAYBACK_SPEED = 16;
|
const MAX_PLAYBACK_SPEED = 16;
|
||||||
@ -16,19 +17,19 @@ const MAX_PLAYBACK_SPEED = 16;
|
|||||||
let playbackSpeed = 1;
|
let playbackSpeed = 1;
|
||||||
|
|
||||||
const updatePlayBackSpeed = () => {
|
const updatePlayBackSpeed = () => {
|
||||||
$('video').playbackRate = playbackSpeed;
|
($('video') as HTMLVideoElement).playbackRate = playbackSpeed;
|
||||||
|
|
||||||
const playbackSpeedElement = $('#playback-speed-value');
|
const playbackSpeedElement = $('#playback-speed-value');
|
||||||
if (playbackSpeedElement) {
|
if (playbackSpeedElement) {
|
||||||
playbackSpeedElement.innerHTML = playbackSpeed;
|
playbackSpeedElement.innerHTML = String(playbackSpeed);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let menu;
|
let menu: Element | null = null;
|
||||||
|
|
||||||
const setupSliderListener = singleton(() => {
|
const setupSliderListener = singleton(() => {
|
||||||
$('#playback-speed-slider').addEventListener('immediate-value-changed', (e) => {
|
$('#playback-speed-slider')?.addEventListener('immediate-value-changed', (e) => {
|
||||||
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
|
playbackSpeed = (e as CustomEvent<{ value: number; }>).detail.value || MIN_PLAYBACK_SPEED;
|
||||||
if (isNaN(playbackSpeed)) {
|
if (isNaN(playbackSpeed)) {
|
||||||
playbackSpeed = 1;
|
playbackSpeed = 1;
|
||||||
}
|
}
|
||||||
@ -43,20 +44,28 @@ const observePopupContainer = () => {
|
|||||||
menu = getSongMenu();
|
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);
|
menu.prepend(slider);
|
||||||
setupSliderListener();
|
setupSliderListener();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe($('ytmusic-popup-container'), {
|
const popupContainer = $('ytmusic-popup-container');
|
||||||
|
if (popupContainer) {
|
||||||
|
observer.observe(popupContainer, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const observeVideo = () => {
|
const observeVideo = () => {
|
||||||
const video = $('video');+
|
const video = $('video') as HTMLVideoElement;
|
||||||
video.addEventListener('ratechange', forcePlaybackRate);
|
video.addEventListener('ratechange', forcePlaybackRate);
|
||||||
video.addEventListener('srcChanged', forcePlaybackRate);
|
video.addEventListener('srcChanged', forcePlaybackRate);
|
||||||
};
|
};
|
||||||
@ -76,17 +85,18 @@ const setupWheelListener = () => {
|
|||||||
|
|
||||||
updatePlayBackSpeed();
|
updatePlayBackSpeed();
|
||||||
// Update slider position
|
// Update slider position
|
||||||
$('#playback-speed-slider').value = playbackSpeed;
|
($('#playback-speed-slider') as HTMLElement & { value: number }).value = playbackSpeed;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function forcePlaybackRate(e) {
|
function forcePlaybackRate(e: Event) {
|
||||||
if (e.target.playbackRate !== playbackSpeed) {
|
const videoElement = (e.target as HTMLVideoElement);
|
||||||
e.target.playbackRate = playbackSpeed;
|
if (videoElement.playbackRate !== playbackSpeed) {
|
||||||
|
videoElement.playbackRate = playbackSpeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
export default () => {
|
||||||
document.addEventListener('apiLoaded', () => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
observePopupContainer();
|
observePopupContainer();
|
||||||
observeVideo();
|
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
|
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) => {
|
export const enabled = () => isEnabled;
|
||||||
enabled = true;
|
|
||||||
|
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>) => {
|
||||||
|
isEnabled = true;
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'volume-hud.css'));
|
injectCSS(win.webContents, path.join(__dirname, 'volume-hud.css'));
|
||||||
|
|
||||||
if (options.globalShortcuts?.volumeUp) {
|
if (options.globalShortcuts?.volumeUp) {
|
||||||
@ -22,5 +25,3 @@ module.exports = (win, options) => {
|
|||||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
|
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);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { debounce } = require('../../providers/decorators');
|
let api: YoutubePlayer;
|
||||||
|
let options: ConfigType<'precise-volume'>;
|
||||||
|
|
||||||
let api;
|
export default (_options: ConfigType<'precise-volume'>) => {
|
||||||
let options;
|
|
||||||
|
|
||||||
module.exports = (_options) => {
|
|
||||||
options = _options;
|
options = _options;
|
||||||
document.addEventListener('apiLoaded', (e) => {
|
document.addEventListener('apiLoaded', (e) => {
|
||||||
api = e.detail;
|
api = e.detail;
|
||||||
ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease));
|
ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease));
|
||||||
ipcRenderer.on('setVolume', (_, value) => setVolume(value));
|
ipcRenderer.on('setVolume', (_, value: number) => setVolume(value));
|
||||||
firstRun();
|
firstRun();
|
||||||
}, { once: true, passive: true });
|
}, { once: true, passive: true });
|
||||||
};
|
};
|
||||||
@ -26,23 +29,22 @@ const writeOptions = debounce(() => {
|
|||||||
setOptions('precise-volume', options);
|
setOptions('precise-volume', options);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const moveVolumeHud = debounce((showVideo) => {
|
export const moveVolumeHud = debounce((showVideo: boolean) => {
|
||||||
const volumeHud = $('#volumeHud');
|
const volumeHud = $('#volumeHud') as HTMLElement | undefined;
|
||||||
if (!volumeHud) {
|
if (!volumeHud) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeHud.style.top = showVideo
|
volumeHud.style.top = showVideo
|
||||||
? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px`
|
? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px`
|
||||||
: 0;
|
: '0';
|
||||||
}, 250);
|
}, 250);
|
||||||
module.exports.moveVolumeHud = moveVolumeHud;
|
|
||||||
|
|
||||||
const hideVolumeHud = debounce((volumeHud) => {
|
const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
|
||||||
volumeHud.style.opacity = 0;
|
volumeHud.style.opacity = '0';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
const hideVolumeSlider = debounce((slider) => {
|
const hideVolumeSlider = debounce((slider: HTMLElement) => {
|
||||||
slider.classList.remove('on-hover');
|
slider.classList.remove('on-hover');
|
||||||
}, 2500);
|
}, 2500);
|
||||||
|
|
||||||
@ -61,14 +63,15 @@ function firstRun() {
|
|||||||
|
|
||||||
setupLocalArrowShortcuts();
|
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);
|
injectVolumeHud(noVid);
|
||||||
if (!noVid) {
|
if (!noVid) {
|
||||||
setupVideoPlayerOnwheel();
|
setupVideoPlayerOnwheel();
|
||||||
if (!isEnabled('video-toggle')) {
|
if (!isEnabled('video-toggle')) {
|
||||||
// Video-toggle handles hud positioning on its own
|
// Video-toggle handles hud positioning on its own
|
||||||
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
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) {
|
if (noVid) {
|
||||||
const position = 'top: 18px; right: 60px;';
|
const position = 'top: 18px; right: 60px;';
|
||||||
const mainStyle = 'font-size: xx-large;';
|
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>`);
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`);
|
||||||
} else {
|
} else {
|
||||||
const position = 'top: 10px; left: 10px;';
|
const position = 'top: 10px; left: 10px;';
|
||||||
const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
|
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>`);
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVolumeHud(volume) {
|
function showVolumeHud(volume: number) {
|
||||||
const volumeHud = $('#volumeHud');
|
const volumeHud = $('#volumeHud') as HTMLElement | undefined;
|
||||||
if (!volumeHud) {
|
if (!volumeHud) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeHud.textContent = `${volume}%`;
|
volumeHud.textContent = `${volume}%`;
|
||||||
volumeHud.style.opacity = 1;
|
volumeHud.style.opacity = '1';
|
||||||
|
|
||||||
hideVolumeHud(volumeHud);
|
hideVolumeHud(volumeHud);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add onwheel event to video player */
|
/** Add onwheel event to video player */
|
||||||
function setupVideoPlayerOnwheel() {
|
function setupVideoPlayerOnwheel() {
|
||||||
$('#main-panel').addEventListener('wheel', (event) => {
|
const panel = $('#main-panel') as HTMLElement | undefined;
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
panel.addEventListener('wheel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Event.deltaY < 0 means wheel-up
|
// Event.deltaY < 0 means wheel-up
|
||||||
changeVolume(event.deltaY < 0);
|
changeVolume(event.deltaY < 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveVolume(volume) {
|
function saveVolume(volume: number) {
|
||||||
options.savedVolume = volume;
|
options.savedVolume = volume;
|
||||||
writeOptions();
|
writeOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add onwheel event to play bar and also track if play bar is hovered */
|
/** Add onwheel event to play bar and also track if play bar is hovered */
|
||||||
function setupPlaybar() {
|
function setupPlaybar() {
|
||||||
const playerbar = $('ytmusic-player-bar');
|
const playerbar = $('ytmusic-player-bar') as HTMLElement | undefined;
|
||||||
|
if (!playerbar) return;
|
||||||
|
|
||||||
playerbar.addEventListener('wheel', (event) => {
|
playerbar.addEventListener('wheel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -148,23 +155,28 @@ function setupSliderObserver() {
|
|||||||
const sliderObserver = new MutationObserver((mutations) => {
|
const sliderObserver = new MutationObserver((mutations) => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
// This checks that volume-slider was manually set
|
// This checks that volume-slider was manually set
|
||||||
if (mutation.oldValue !== mutation.target.value
|
const target = mutation.target as HTMLInputElement;
|
||||||
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
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
|
// Diff>4 means it was manually set
|
||||||
setTooltip(mutation.target.value);
|
setTooltip(targetValueNumeric);
|
||||||
saveVolume(mutation.target.value);
|
saveVolume(targetValueNumeric);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slider = $('#volume-slider');
|
||||||
|
if (!slider) return;
|
||||||
|
|
||||||
// Observing only changes in 'value' of volume-slider
|
// Observing only changes in 'value' of volume-slider
|
||||||
sliderObserver.observe($('#volume-slider'), {
|
sliderObserver.observe(slider, {
|
||||||
attributeFilter: ['value'],
|
attributeFilter: ['value'],
|
||||||
attributeOldValue: true,
|
attributeOldValue: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume(value) {
|
function setVolume(value: number) {
|
||||||
api.setVolume(value);
|
api.setVolume(value);
|
||||||
// Save the new volume
|
// Save the new volume
|
||||||
saveVolume(value);
|
saveVolume(value);
|
||||||
@ -181,7 +193,7 @@ function setVolume(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** If (toIncrease = false) then volume decrease */
|
/** If (toIncrease = false) then volume decrease */
|
||||||
function changeVolume(toIncrease) {
|
function changeVolume(toIncrease: boolean) {
|
||||||
// Apply volume change if valid
|
// Apply volume change if valid
|
||||||
const steps = Number(options.steps || 1);
|
const steps = Number(options.steps || 1);
|
||||||
setVolume(toIncrease
|
setVolume(toIncrease
|
||||||
@ -190,17 +202,20 @@ function changeVolume(toIncrease) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateVolumeSlider() {
|
function updateVolumeSlider() {
|
||||||
|
const savedVolume = options.savedVolume ?? 0;
|
||||||
// Slider value automatically rounds to multiples of 5
|
// Slider value automatically rounds to multiples of 5
|
||||||
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
|
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
|
||||||
$(slider).value
|
($(slider) as HTMLInputElement).value
|
||||||
= options.savedVolume > 0 && options.savedVolume < 5
|
= String(savedVolume > 0 && savedVolume < 5
|
||||||
? 5
|
? 5
|
||||||
: options.savedVolume;
|
: savedVolume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVolumeSlider() {
|
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
|
// This class display the volume slider if not in minimized mode
|
||||||
slider.classList.add('on-hover');
|
slider.classList.add('on-hover');
|
||||||
|
|
||||||
@ -215,16 +230,16 @@ const tooltipTargets = [
|
|||||||
'#expand-volume',
|
'#expand-volume',
|
||||||
];
|
];
|
||||||
|
|
||||||
function setTooltip(volume) {
|
function setTooltip(volume: number) {
|
||||||
for (const target of tooltipTargets) {
|
for (const target of tooltipTargets) {
|
||||||
$(target).title = `${volume}%`;
|
($(target) as HTMLElement).title = `${volume}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLocalArrowShortcuts() {
|
function setupLocalArrowShortcuts() {
|
||||||
if (options.arrowsShortcut) {
|
if (options.arrowsShortcut) {
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
if ($('ytmusic-search-box').opened) {
|
if (($('ytmusic-search-box') as (HTMLElement & { opened: boolean }) | null)?.opened) {
|
||||||
return;
|
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');
|
import { enabled } from './back';
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
|
||||||
|
|
||||||
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) {
|
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
|
// Dynamically change setting if plugin is enabled
|
||||||
if (enabled()) {
|
if (enabled()) {
|
||||||
win.webContents.send('setOptions', changedOptions);
|
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',
|
label: 'Local Arrowkeys Controls',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
@ -40,9 +45,9 @@ module.exports = (win, options) => [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Helper function for globalShortcuts prompt
|
// 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({
|
const output = await prompt({
|
||||||
title: 'Volume Steps',
|
title: 'Volume Steps',
|
||||||
label: 'Choose Volume Increase/Decrease 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({
|
const output = await prompt({
|
||||||
title: 'Global Volume Keybinds',
|
title: 'Global Volume Keybinds',
|
||||||
label: 'Choose Global Volume Keybinds:',
|
label: 'Choose Global Volume Keybinds:',
|
||||||
@ -71,9 +76,12 @@ async function promptGlobalShortcuts(win, options, item) {
|
|||||||
}, win);
|
}, win);
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
const newGlobalShortcuts = {};
|
const newGlobalShortcuts: {
|
||||||
|
volumeUp: string;
|
||||||
|
volumeDown: string;
|
||||||
|
} = { volumeUp: '', volumeDown: '' };
|
||||||
for (const { value, accelerator } of output) {
|
for (const { value, accelerator } of output) {
|
||||||
newGlobalShortcuts[value] = accelerator;
|
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
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 = () => {
|
export default () => {
|
||||||
ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => await dialog.showMessageBox({
|
ipcMain.handle('qualityChanger', async (_, qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
buttons: qualityLabels,
|
buttons: qualityLabels,
|
||||||
defaultId: currentIndex,
|
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);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,32 +11,19 @@ const qualitySettingsButton = ElementFromFile(
|
|||||||
templatePath(__dirname, 'qualitySettingsTemplate.html'),
|
templatePath(__dirname, 'qualitySettingsTemplate.html'),
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = () => {
|
function setup(event: CustomEvent<YoutubePlayer>) {
|
||||||
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,
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
const api = event.detail;
|
const api = event.detail;
|
||||||
|
|
||||||
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
|
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
|
||||||
|
|
||||||
qualitySettingsButton.addEventListener('click', function chooseQuality() {
|
qualitySettingsButton.addEventListener('click', function chooseQuality() {
|
||||||
setTimeout(() => $('#player').click());
|
setTimeout(() => $('#player')?.click());
|
||||||
|
|
||||||
const qualityLevels = api.getAvailableQualityLevels();
|
const qualityLevels = api.getAvailableQualityLevels();
|
||||||
|
|
||||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
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) {
|
if (promise.response === -1) {
|
||||||
return;
|
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');
|
import { BrowserWindow, globalShortcut } from 'electron';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
const electronLocalshortcut = require('electron-localshortcut');
|
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, () => {
|
globalShortcut.register(shortcut, () => {
|
||||||
action(webContents);
|
action(webContents);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _registerLocalShortcut(win, shortcut, action) {
|
function _registerLocalShortcut(win: BrowserWindow, shortcut: string, action: (webContents: Electron.WebContents) => void) {
|
||||||
electronLocalshortcut.register(win, shortcut, () => {
|
electronLocalshortcut.register(win, shortcut, () => {
|
||||||
action(win.webContents);
|
action(win.webContents);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerShortcuts(win, options) {
|
function registerShortcuts(win: BrowserWindow, options: ConfigType<'shortcuts'>) {
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const { playPause, next, previous, search } = songControls;
|
const { playPause, next, previous, search } = songControls;
|
||||||
|
|
||||||
@ -39,28 +41,29 @@ function registerShortcuts(win, options) {
|
|||||||
const shortcutOptions = { global, local };
|
const shortcutOptions = { global, local };
|
||||||
|
|
||||||
for (const optionType in shortcutOptions) {
|
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) {
|
for (const action in container) {
|
||||||
if (!container[action]) {
|
if (!container[action]) {
|
||||||
continue; // Action accelerator is empty
|
continue; // Action accelerator is empty
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Registering ${type} shortcut`, container[action], ':', action);
|
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);
|
console.warn('Invalid action', action);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'global') {
|
if (type === 'global') {
|
||||||
_registerGlobalShortcut(win.webContents, container[action], songControls[action]);
|
_registerGlobalShortcut(win.webContents, container[action], actionCallback);
|
||||||
} else { // Type === "local"
|
} 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');
|
import { BrowserWindow } from 'electron';
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
|
||||||
|
|
||||||
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',
|
label: 'Set Global Song Controls',
|
||||||
click: () => promptKeybind(options, win),
|
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) {
|
if (key && newValue !== null) {
|
||||||
options[key] = newValue;
|
options[key] = newValue;
|
||||||
}
|
}
|
||||||
@ -25,9 +34,9 @@ function setOption(options, key = null, newValue = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for keybind prompt
|
// 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({
|
const output = await prompt({
|
||||||
title: 'Global Keybinds',
|
title: 'Global Keybinds',
|
||||||
label: 'Choose Global Keybinds for Songs Control:',
|
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');
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
const mpris = require('mpris-service');
|
|
||||||
|
|
||||||
const registerCallback = require('../../providers/song-info');
|
import mpris, { Track } from 'mpris-service';
|
||||||
const getSongControls = require('../../providers/song-controls');
|
|
||||||
const config = require('../../config');
|
import registerCallback from '../../providers/song-info';
|
||||||
|
import getSongControls from '../../providers/song-controls';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
function setupMPRIS() {
|
function setupMPRIS() {
|
||||||
return mpris({
|
const instance = new mpris({
|
||||||
name: 'youtube-music',
|
name: 'youtube-music',
|
||||||
identity: 'YouTube Music',
|
identity: 'YouTube Music',
|
||||||
canRaise: true,
|
|
||||||
supportedUriSchemes: ['https'],
|
|
||||||
supportedMimeTypes: ['audio/mpeg'],
|
supportedMimeTypes: ['audio/mpeg'],
|
||||||
supportedInterfaces: ['player'],
|
supportedInterfaces: ['player'],
|
||||||
desktopEntry: 'youtube-music',
|
|
||||||
});
|
});
|
||||||
|
instance.canRaise = true;
|
||||||
|
instance.supportedUriSchemes = ['https'];
|
||||||
|
instance.desktopEntry = 'youtube-music';
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Electron.BrowserWindow} win */
|
function registerMPRIS(win: BrowserWindow) {
|
||||||
function registerMPRIS(win) {
|
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
|
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
|
||||||
try {
|
try {
|
||||||
const secToMicro = (n) => Math.round(Number(n) * 1e6);
|
// TODO: "Typing" for this arguments
|
||||||
const microToSec = (n) => Math.round(Number(n) / 1e6);
|
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 seekTo = (e: { position: unknown }) => win.webContents.send('seekTo', microToSec(e.position));
|
||||||
const seekBy = (o) => win.webContents.send('seekBy', microToSec(o));
|
const seekBy = (o: unknown) => win.webContents.send('seekBy', microToSec(o));
|
||||||
|
|
||||||
const player = setupMPRIS();
|
const player = setupMPRIS();
|
||||||
|
|
||||||
@ -37,12 +39,12 @@ function registerMPRIS(win) {
|
|||||||
win.webContents.send('setupVolumeChangedListener', 'mpris');
|
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;
|
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) {
|
switch (mode) {
|
||||||
case 'NONE': {
|
case 'NONE': {
|
||||||
player.loopStatus = mpris.LOOP_STATUS_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
|
// SwitchRepeat cycles between states in that order
|
||||||
const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK];
|
const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK];
|
||||||
const currentIndex = switches.indexOf(player.loopStatus);
|
const currentIndex = switches.indexOf(player.loopStatus);
|
||||||
@ -75,8 +77,6 @@ function registerMPRIS(win) {
|
|||||||
songControls.switchRepeat(delta);
|
songControls.switchRepeat(delta);
|
||||||
});
|
});
|
||||||
|
|
||||||
player.getPosition = () => secToMicro(currentSeconds);
|
|
||||||
|
|
||||||
player.on('raise', () => {
|
player.on('raise', () => {
|
||||||
win.setSkipTaskbar(false);
|
win.setSkipTaskbar(false);
|
||||||
win.show();
|
win.show();
|
||||||
@ -114,7 +114,7 @@ function registerMPRIS(win) {
|
|||||||
let mprisVolNewer = false;
|
let mprisVolNewer = false;
|
||||||
let autoUpdate = false;
|
let autoUpdate = false;
|
||||||
ipcMain.on('volumeChanged', (_, newVol) => {
|
ipcMain.on('volumeChanged', (_, newVol) => {
|
||||||
if (Number.parseInt(player.volume * 100) !== newVol) {
|
if (~~(player.volume * 100) !== newVol) {
|
||||||
if (mprisVolNewer) {
|
if (mprisVolNewer) {
|
||||||
mprisVolNewer = false;
|
mprisVolNewer = false;
|
||||||
autoUpdate = false;
|
autoUpdate = false;
|
||||||
@ -130,8 +130,8 @@ function registerMPRIS(win) {
|
|||||||
player.on('volume', (newVolume) => {
|
player.on('volume', (newVolume) => {
|
||||||
if (config.plugins.isEnabled('precise-volume')) {
|
if (config.plugins.isEnabled('precise-volume')) {
|
||||||
// With precise volume we can set the volume to the exact value.
|
// With precise volume we can set the volume to the exact value.
|
||||||
const newVol = Number.parseInt(newVolume * 100);
|
const newVol = ~~(newVolume * 100);
|
||||||
if (Number.parseInt(player.volume * 100) !== newVol && !autoUpdate) {
|
if (~~(player.volume * 100) !== newVol && !autoUpdate) {
|
||||||
mprisVolNewer = true;
|
mprisVolNewer = true;
|
||||||
autoUpdate = false;
|
autoUpdate = false;
|
||||||
win.webContents.send('setVolume', newVol);
|
win.webContents.send('setVolume', newVol);
|
||||||
@ -155,9 +155,9 @@ function registerMPRIS(win) {
|
|||||||
|
|
||||||
registerCallback((songInfo) => {
|
registerCallback((songInfo) => {
|
||||||
if (player) {
|
if (player) {
|
||||||
const data = {
|
const data: Track = {
|
||||||
'mpris:length': secToMicro(songInfo.songDuration),
|
'mpris:length': secToMicro(songInfo.songDuration),
|
||||||
'mpris:artUrl': songInfo.imageSrc,
|
'mpris:artUrl': songInfo.imageSrc ?? undefined,
|
||||||
'xesam:title': songInfo.title,
|
'xesam:title': songInfo.title,
|
||||||
'xesam:url': songInfo.url,
|
'xesam:url': songInfo.url,
|
||||||
'xesam:artist': [songInfo.artist],
|
'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 = ConfigType<'skip-silences'>;
|
||||||
type SkipSilencesOptions = typeof SkipSilencesOptionsObj;
|
|
||||||
|
|
||||||
export default (options: SkipSilencesOptions) => {
|
export default (options: SkipSilencesOptions) => {
|
||||||
let isSilent = false;
|
let isSilent = false;
|
||||||
|
|||||||
@ -1,26 +1,31 @@
|
|||||||
const { ipcMain } = require('electron');
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
const is = require('electron-is');
|
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 } = {
|
const { apiURL, categories } = {
|
||||||
...defaultConfig.plugins.sponsorblock,
|
...defaultConfig.plugins.sponsorblock,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.on('video-src-changed', async (_, data) => {
|
ipcMain.on('video-src-changed', async (_, data: string) => {
|
||||||
videoID = JSON.parse(data)?.videoDetails?.videoId;
|
videoID = (JSON.parse(data) as GetPlayerResponse)?.videoDetails?.videoId;
|
||||||
const segments = await fetchSegments(apiURL, categories);
|
const segments = await fetchSegments(apiURL, categories);
|
||||||
win.webContents.send('sponsorblock-skip', segments);
|
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(
|
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
|
||||||
categories,
|
categories,
|
||||||
)}`;
|
)}`;
|
||||||
@ -36,7 +41,7 @@ const fetchSegments = async (apiURL, categories) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = await resp.json();
|
const segments = await resp.json() as SkipSegment[];
|
||||||
return sortSegments(
|
return sortSegments(
|
||||||
segments.map((submission) => submission.segment),
|
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], … ]
|
// Segments are an array [ [start, end], … ]
|
||||||
module.exports.sortSegments = (segments) => {
|
import { Segment } from './types';
|
||||||
|
|
||||||
|
export const sortSegments = (segments: Segment[]) => {
|
||||||
segments.sort((segment1, segment2) =>
|
segments.sort((segment1, segment2) =>
|
||||||
segment1[0] === segment2[0]
|
segment1[0] === segment2[0]
|
||||||
? segment1[1] - segment2[1]
|
? segment1[1] - segment2[1]
|
||||||
: segment1[0] - segment2[0],
|
: segment1[0] - segment2[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const compiledSegments = [];
|
const compiledSegments: Segment[] = [];
|
||||||
let currentSegment;
|
let currentSegment: Segment | undefined;
|
||||||
|
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
if (!currentSegment) {
|
if (!currentSegment) {
|
||||||
@ -24,7 +26,9 @@ module.exports.sortSegments = (segments) => {
|
|||||||
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
|
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentSegment) {
|
||||||
compiledSegments.push(currentSegment);
|
compiledSegments.push(currentSegment);
|
||||||
|
}
|
||||||
|
|
||||||
return compiledSegments;
|
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);
|
const { playPause, next, previous, dislike, like } = getSongControls(win);
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// 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}`));
|
}).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('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||||
ipcMain.on('timeChanged', (_, t: number) => {
|
ipcMain.on('timeChanged', (_, t: number) => {
|
||||||
if (!data.title) {
|
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 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 = (
|
export const fileExists = (
|
||||||
path: fs.PathLike,
|
path: fs.PathLike,
|
||||||
@ -48,7 +48,7 @@ export const fileExists = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cssToInject = new Map();
|
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) {
|
if (cssToInject.size === 0) {
|
||||||
setupCssInjection(webContents);
|
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');
|
import { ElementFromFile, templatePath } from '../utils';
|
||||||
const { setOptions, isEnabled } = require('../../config/plugins');
|
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);
|
return document.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
let options;
|
let options: ConfigType<'video-toggle'>;
|
||||||
let player;
|
let player: HTMLElement & { videoMode_: boolean };
|
||||||
let video;
|
let video: HTMLVideoElement;
|
||||||
let api;
|
let api: YoutubePlayer;
|
||||||
|
|
||||||
const switchButtonDiv = ElementFromFile(
|
const switchButtonDiv = ElementFromFile(
|
||||||
templatePath(__dirname, 'button_template.html'),
|
templatePath(__dirname, 'button_template.html'),
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = (_options) => {
|
export default (_options: ConfigType<'video-toggle'>) => {
|
||||||
if (_options.forceHide) {
|
if (_options.forceHide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (_options.mode) {
|
switch (_options.mode) {
|
||||||
case 'native': {
|
case 'native': {
|
||||||
$('ytmusic-player-page').setAttribute('has-av-switcher');
|
$('ytmusic-player-page')?.setAttribute('has-av-switcher', '');
|
||||||
$('ytmusic-player').setAttribute('has-av-switcher');
|
$('ytmusic-player')?.setAttribute('has-av-switcher', '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'disabled': {
|
case 'disabled': {
|
||||||
$('ytmusic-player-page').removeAttribute('has-av-switcher');
|
$('ytmusic-player-page')?.removeAttribute('has-av-switcher');
|
||||||
$('ytmusic-player').removeAttribute('has-av-switcher');
|
$('ytmusic-player')?.removeAttribute('has-av-switcher');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +47,15 @@ module.exports = (_options) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function setup(e) {
|
function setup(e: CustomEvent<YoutubePlayer>) {
|
||||||
api = e.detail;
|
api = e.detail;
|
||||||
player = $('ytmusic-player');
|
player = $('ytmusic-player') as typeof player;
|
||||||
video = $('video');
|
video = $('video') as HTMLVideoElement;
|
||||||
|
|
||||||
$('#main-panel').append(switchButtonDiv);
|
($('#main-panel') as HTMLVideoElement).append(switchButtonDiv);
|
||||||
|
|
||||||
if (options.hideVideo) {
|
if (options.hideVideo) {
|
||||||
$('.video-switch-button-checkbox').checked = false;
|
($('.video-switch-button-checkbox') as HTMLInputElement).checked = false;
|
||||||
changeDisplay(false);
|
changeDisplay(false);
|
||||||
forcePlaybackMode();
|
forcePlaybackMode();
|
||||||
// Fix black video
|
// Fix black video
|
||||||
@ -60,8 +64,9 @@ function setup(e) {
|
|||||||
|
|
||||||
// Button checked = show video
|
// Button checked = show video
|
||||||
switchButtonDiv.addEventListener('change', (e) => {
|
switchButtonDiv.addEventListener('change', (e) => {
|
||||||
options.hideVideo = !e.target.checked;
|
const target = e.target as HTMLInputElement;
|
||||||
changeDisplay(e.target.checked);
|
options.hideVideo = target.checked;
|
||||||
|
changeDisplay(target.checked);
|
||||||
setOptions('video-toggle', options);
|
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.style.margin = showVideo ? '' : 'auto 0px';
|
||||||
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
||||||
|
|
||||||
$('#song-video.ytmusic-player').style.display = showVideo ? 'block' : 'none';
|
$('#song-video.ytmusic-player')!.style.display = showVideo ? 'block' : 'none';
|
||||||
$('#song-image').style.display = showVideo ? 'none' : 'block';
|
$('#song-image')!.style.display = showVideo ? 'none' : 'block';
|
||||||
|
|
||||||
if (showVideo && !video.style.top) {
|
if (showVideo && !video.style.top) {
|
||||||
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
||||||
@ -108,12 +113,12 @@ function videoStarted() {
|
|||||||
// Hide toggle button
|
// Hide toggle button
|
||||||
switchButtonDiv.style.display = 'none';
|
switchButtonDiv.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
// Switch to high res thumbnail
|
// Switch to high-res thumbnail
|
||||||
forceThumbnail($('#song-image img'));
|
forceThumbnail($('#song-image img') as HTMLImageElement);
|
||||||
// Show toggle button
|
// Show toggle button
|
||||||
switchButtonDiv.style.display = 'initial';
|
switchButtonDiv.style.display = 'initial';
|
||||||
// Change display to video mode if video exist & video is hidden & option.hideVideo = false
|
// 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);
|
changeDisplay(true);
|
||||||
} else {
|
} else {
|
||||||
moveVolumeHud(!options.hideVideo);
|
moveVolumeHud(!options.hideVideo);
|
||||||
@ -126,9 +131,10 @@ function videoStarted() {
|
|||||||
function forcePlaybackMode() {
|
function forcePlaybackMode() {
|
||||||
const playbackModeObserver = new MutationObserver((mutations) => {
|
const playbackModeObserver = new MutationObserver((mutations) => {
|
||||||
for (const mutation of 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();
|
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) {
|
for (const mutation of mutations) {
|
||||||
if (!mutation.target.src.startsWith('data:')) {
|
const target = mutation.target as HTMLImageElement;
|
||||||
|
if (!target.src.startsWith('data:')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
forceThumbnail(mutation.target);
|
forceThumbnail(target);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
playbackModeObserver.observe($('#song-image img'), { attributeFilter: ['src'] });
|
playbackModeObserver.observe($('#song-image img')!, { attributeFilter: ['src'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceThumbnail(img) {
|
function forceThumbnail(img: HTMLImageElement) {
|
||||||
const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails;
|
const thumbnails: ThumbnailElement[] = ($('#movie_player') as unknown as YoutubePlayer).getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? [];
|
||||||
if (thumbnails && thumbnails.length > 0) {
|
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',
|
label: 'Mode',
|
||||||
submenu: [
|
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 = {
|
const optionsWithDefaults = {
|
||||||
...defaultConfig.plugins.visualizer,
|
...defaultConfig.plugins.visualizer,
|
||||||
...options,
|
...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(
|
document.addEventListener(
|
||||||
'audioCanPlay',
|
'audioCanPlay',
|
||||||
(e) => {
|
(e) => {
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video') as (HTMLVideoElement & { captureStream(): MediaStream; });
|
||||||
const visualizerContainer = document.querySelector('#player');
|
const visualizerContainer = document.querySelector('#player') as HTMLElement;
|
||||||
|
|
||||||
let canvas = document.querySelector('#visualizer');
|
let canvas = document.querySelector('#visualizer') as HTMLCanvasElement;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
canvas.id = 'visualizer';
|
canvas.id = 'visualizer';
|
||||||
@ -33,17 +42,17 @@ module.exports = (options) => {
|
|||||||
gainNode.gain.value = 1.25;
|
gainNode.gain.value = 1.25;
|
||||||
e.detail.audioSource.connect(gainNode);
|
e.detail.audioSource.connect(gainNode);
|
||||||
|
|
||||||
const visualizer = new VisualizerType(
|
const visualizer = new visualizerType(
|
||||||
e.detail.audioContext,
|
e.detail.audioContext,
|
||||||
e.detail.audioSource,
|
e.detail.audioSource,
|
||||||
visualizerContainer,
|
visualizerContainer,
|
||||||
canvas,
|
canvas,
|
||||||
gainNode,
|
gainNode,
|
||||||
video.captureStream(),
|
video.captureStream(),
|
||||||
optionsWithDefaults[optionsWithDefaults.type],
|
optionsWithDefaults,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resizeVisualizer = (width, height) => {
|
const resizeVisualizer = (width: number, height: number) => {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
visualizer.resize(width, height);
|
visualizer.resize(width, height);
|
||||||
};
|
};
|
||||||
@ -1,13 +1,19 @@
|
|||||||
const { readdirSync } = require('node:fs');
|
import { readdirSync } from 'node:fs';
|
||||||
const path = require('node:path');
|
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(
|
const visualizerTypes = readdirSync(path.join(__dirname, 'visualizers')).map(
|
||||||
(filename) => path.parse(filename).name,
|
(filename) => path.parse(filename).name,
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
export default (win: BrowserWindow, options: ConfigType<'visualizer'>): MenuTemplate => [
|
||||||
{
|
{
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
submenu: visualizerTypes.map((visualizerType) => ({
|
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;
|
getVideoAspectRatio: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getPreferredQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getPreferredQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getPlaybackQualityLabel: <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;
|
onAdUxClicked: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getFeedbackProductData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getFeedbackProductData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getStoryboardFrame: <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;
|
getStoryboardLevel: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getNumberOfStoryboardLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getNumberOfStoryboardLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getCaptionWindowContainerId: <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;
|
addUtcCueRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
showAirplayPicker: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
showAirplayPicker: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
dispatchReduxAction: <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;
|
unMute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
setVolume: <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;
|
seekTo: (seconds: number) => void;
|
||||||
getPlayerMode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getPlayerMode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getPlayerState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getPlayerState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getAvailablePlaybackRates: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getAvailablePlaybackRates: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getPlaybackQuality: () => string;
|
||||||
setPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
setPlaybackQuality: (quality: string) => void;
|
||||||
getAvailableQualityLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getAvailableQualityLevels: () => string[];
|
||||||
getCurrentTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getCurrentTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
getDuration: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
getDuration: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
addEventListener: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
addEventListener: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
|||||||
Reference in New Issue
Block a user