mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
16
.eslintrc.js
16
.eslintrc.js
@ -2,14 +2,26 @@ module.exports = {
|
|||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:import/recommended',
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||||
],
|
],
|
||||||
plugins: ['import'],
|
plugins: ['@typescript-eslint', 'import'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest'
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'arrow-parens': ['error', 'always'],
|
'arrow-parens': ['error', 'always'],
|
||||||
'object-curly-spacing': ['error', 'always'],
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'@typescript-eslint/no-floating-promises': 'off',
|
||||||
|
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
'import/first': 'error',
|
'import/first': 'error',
|
||||||
'import/newline-after-import': 'error',
|
'import/newline-after-import': 'error',
|
||||||
'import/no-default-export': 'off',
|
'import/no-default-export': 'off',
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
/pack
|
||||||
electron-builder.yml
|
electron-builder.yml
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@ -1,22 +1,38 @@
|
|||||||
|
export interface WindowSizeConfig {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
'window-size': {
|
'window-size': {
|
||||||
width: 1100,
|
width: 1100,
|
||||||
height: 550,
|
height: 550,
|
||||||
},
|
},
|
||||||
|
'window-maximized': false,
|
||||||
|
'window-position': {
|
||||||
|
x: -1,
|
||||||
|
y: -1,
|
||||||
|
},
|
||||||
'url': 'https://music.youtube.com',
|
'url': 'https://music.youtube.com',
|
||||||
'options': {
|
'options': {
|
||||||
tray: false,
|
tray: false,
|
||||||
appVisible: true,
|
appVisible: true,
|
||||||
autoUpdates: true,
|
autoUpdates: true,
|
||||||
|
alwaysOnTop: false,
|
||||||
hideMenu: false,
|
hideMenu: false,
|
||||||
|
hideMenuWarned: false,
|
||||||
startAtLogin: false,
|
startAtLogin: false,
|
||||||
disableHardwareAcceleration: false,
|
disableHardwareAcceleration: false,
|
||||||
|
removeUpgradeButton: false,
|
||||||
restartOnConfigChanges: false,
|
restartOnConfigChanges: false,
|
||||||
trayClickPlayPause: false,
|
trayClickPlayPause: false,
|
||||||
autoResetAppCache: false,
|
autoResetAppCache: false,
|
||||||
resumeOnStart: true,
|
resumeOnStart: true,
|
||||||
|
likeButtons: '',
|
||||||
proxy: '',
|
proxy: '',
|
||||||
startingPage: '',
|
startingPage: '',
|
||||||
|
overrideUserAgent: false,
|
||||||
|
themes: {} as string[],
|
||||||
},
|
},
|
||||||
'plugins': {
|
'plugins': {
|
||||||
// Enabled plugins
|
// Enabled plugins
|
||||||
@ -26,7 +42,9 @@ const defaultConfig = {
|
|||||||
'adblocker': {
|
'adblocker': {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
|
blocker: 'In player',
|
||||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||||
|
disableDefaultLists: [],
|
||||||
},
|
},
|
||||||
// Disabled plugins
|
// Disabled plugins
|
||||||
'shortcuts': {
|
'shortcuts': {
|
||||||
@ -92,11 +110,14 @@ const defaultConfig = {
|
|||||||
forceHide: false,
|
forceHide: false,
|
||||||
},
|
},
|
||||||
'picture-in-picture': {
|
'picture-in-picture': {
|
||||||
enabled: false,
|
'enabled': false,
|
||||||
alwaysOnTop: true,
|
'alwaysOnTop': true,
|
||||||
savePosition: true,
|
'savePosition': true,
|
||||||
saveSize: false,
|
'saveSize': false,
|
||||||
hotkey: 'P',
|
'hotkey': 'P',
|
||||||
|
'pip-position': [10, 10],
|
||||||
|
'pip-size': [450, 275],
|
||||||
|
'isInPiP': false,
|
||||||
},
|
},
|
||||||
'captions-selector': {
|
'captions-selector': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -181,4 +202,4 @@ const defaultConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = defaultConfig;
|
export default defaultConfig;
|
||||||
@ -1,214 +0,0 @@
|
|||||||
const { ipcRenderer, ipcMain } = require('electron');
|
|
||||||
|
|
||||||
const defaultConfig = require('./defaults');
|
|
||||||
const { getOptions, setOptions, setMenuOptions } = require('./plugins');
|
|
||||||
|
|
||||||
const { sendToFront } = require('../providers/app-controls');
|
|
||||||
|
|
||||||
const activePlugins = {};
|
|
||||||
/**
|
|
||||||
* [!IMPORTANT!]
|
|
||||||
* The method is **sync** in the main process and **async** in the renderer process.
|
|
||||||
*/
|
|
||||||
module.exports.getActivePlugins
|
|
||||||
= process.type === 'renderer'
|
|
||||||
? async () => ipcRenderer.invoke('get-active-plugins')
|
|
||||||
: () => activePlugins;
|
|
||||||
|
|
||||||
if (process.type === 'browser') {
|
|
||||||
ipcMain.handle('get-active-plugins', this.getActivePlugins);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [!IMPORTANT!]
|
|
||||||
* The method is **sync** in the main process and **async** in the renderer process.
|
|
||||||
*/
|
|
||||||
module.exports.isActive
|
|
||||||
= process.type === 'renderer'
|
|
||||||
? async (plugin) =>
|
|
||||||
plugin in (await ipcRenderer.invoke('get-active-plugins'))
|
|
||||||
: (plugin) => plugin in activePlugins;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to create a dynamic synced config for plugins.
|
|
||||||
*
|
|
||||||
* [!IMPORTANT!]
|
|
||||||
* The methods are **sync** in the main process and **async** in the renderer process.
|
|
||||||
*
|
|
||||||
* @param {string} name - The name of the plugin.
|
|
||||||
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
|
|
||||||
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { PluginConfig } = require("../../config/dynamic");
|
|
||||||
* const config = new PluginConfig("plugin-name", { enableFront: true });
|
|
||||||
* module.exports = { ...config };
|
|
||||||
*
|
|
||||||
* // or
|
|
||||||
*
|
|
||||||
* module.exports = (win, options) => {
|
|
||||||
* const config = new PluginConfig("plugin-name", {
|
|
||||||
* enableFront: true,
|
|
||||||
* initialOptions: options,
|
|
||||||
* });
|
|
||||||
* setupMyPlugin(win, config);
|
|
||||||
* };
|
|
||||||
*/
|
|
||||||
module.exports.PluginConfig = class PluginConfig {
|
|
||||||
#name;
|
|
||||||
#config;
|
|
||||||
#defaultConfig;
|
|
||||||
#enableFront;
|
|
||||||
|
|
||||||
#subscribers = {};
|
|
||||||
#allSubscribers = [];
|
|
||||||
|
|
||||||
constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
|
|
||||||
const pluginDefaultConfig = defaultConfig.plugins[name] || {};
|
|
||||||
const pluginConfig = initialOptions || getOptions(name) || {};
|
|
||||||
|
|
||||||
this.#name = name;
|
|
||||||
this.#enableFront = enableFront;
|
|
||||||
this.#defaultConfig = pluginDefaultConfig;
|
|
||||||
this.#config = { ...pluginDefaultConfig, ...pluginConfig };
|
|
||||||
|
|
||||||
if (this.#enableFront) {
|
|
||||||
this.#setupFront();
|
|
||||||
}
|
|
||||||
|
|
||||||
activePlugins[name] = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get = (option) => this.#config[option];
|
|
||||||
|
|
||||||
set = (option, value) => {
|
|
||||||
this.#config[option] = value;
|
|
||||||
this.#onChange(option);
|
|
||||||
this.#save();
|
|
||||||
};
|
|
||||||
|
|
||||||
toggle = (option) => {
|
|
||||||
this.#config[option] = !this.#config[option];
|
|
||||||
this.#onChange(option);
|
|
||||||
this.#save();
|
|
||||||
};
|
|
||||||
|
|
||||||
getAll = () => ({ ...this.#config });
|
|
||||||
|
|
||||||
setAll = (options) => {
|
|
||||||
if (!options || typeof options !== 'object') {
|
|
||||||
throw new Error('Options must be an object.');
|
|
||||||
}
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
for (const [key, value] of Object.entries(options)) {
|
|
||||||
if (this.#config[key] !== value) {
|
|
||||||
this.#config[key] = value;
|
|
||||||
this.#onChange(key, false);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
for (const fn of this.#allSubscribers) {
|
|
||||||
fn(this.#config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#save();
|
|
||||||
};
|
|
||||||
|
|
||||||
getDefaultConfig = () => this.#defaultConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
|
|
||||||
*
|
|
||||||
* Used for options that require a restart to take effect.
|
|
||||||
*/
|
|
||||||
setAndMaybeRestart = (option, value) => {
|
|
||||||
this.#config[option] = value;
|
|
||||||
setMenuOptions(this.#name, this.#config);
|
|
||||||
this.#onChange(option);
|
|
||||||
};
|
|
||||||
|
|
||||||
subscribe = (valueName, fn) => {
|
|
||||||
this.#subscribers[valueName] = fn;
|
|
||||||
};
|
|
||||||
|
|
||||||
subscribeAll = (fn) => {
|
|
||||||
this.#allSubscribers.push(fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Called only from back */
|
|
||||||
#save() {
|
|
||||||
setOptions(this.#name, this.#config);
|
|
||||||
}
|
|
||||||
|
|
||||||
#onChange(valueName, single = true) {
|
|
||||||
this.#subscribers[valueName]?.(this.#config[valueName]);
|
|
||||||
if (single) {
|
|
||||||
for (const fn of this.#allSubscribers) {
|
|
||||||
fn(this.#config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#setupFront() {
|
|
||||||
const ignoredMethods = ['subscribe', 'subscribeAll'];
|
|
||||||
|
|
||||||
if (process.type === 'renderer') {
|
|
||||||
for (const [fnName, fn] of Object.entries(this)) {
|
|
||||||
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this[fnName] = async (...args) => await ipcRenderer.invoke(
|
|
||||||
`${this.#name}-config-${fnName}`,
|
|
||||||
...args,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.subscribe = (valueName, fn) => {
|
|
||||||
if (valueName in this.#subscribers) {
|
|
||||||
console.error(`Already subscribed to ${valueName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#subscribers[valueName] = fn;
|
|
||||||
ipcRenderer.on(
|
|
||||||
`${this.#name}-config-changed-${valueName}`,
|
|
||||||
(_, value) => {
|
|
||||||
fn(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ipcRenderer.send(`${this.#name}-config-subscribe`, valueName);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.subscribeAll = (fn) => {
|
|
||||||
ipcRenderer.on(`${this.#name}-config-changed`, (_, value) => {
|
|
||||||
fn(value);
|
|
||||||
});
|
|
||||||
ipcRenderer.send(`${this.#name}-config-subscribe-all`);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (process.type === 'browser') {
|
|
||||||
for (const [fnName, fn] of Object.entries(this)) {
|
|
||||||
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(`${this.#name}-config-${fnName}`, (_, ...args) => fn(...args));
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.on(`${this.#name}-config-subscribe`, (_, valueName) => {
|
|
||||||
this.subscribe(valueName, (value) => {
|
|
||||||
sendToFront(`${this.#name}-config-changed-${valueName}`, value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on(`${this.#name}-config-subscribe-all`, () => {
|
|
||||||
this.subscribeAll((value) => {
|
|
||||||
sendToFront(`${this.#name}-config-changed`, value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
240
config/dynamic.ts
Normal file
240
config/dynamic.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { ipcMain, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import defaultConfig from './defaults';
|
||||||
|
|
||||||
|
import { getOptions, setMenuOptions, setOptions } from './plugins';
|
||||||
|
|
||||||
|
|
||||||
|
import { sendToFront } from '../providers/app-controls';
|
||||||
|
import { Entries } from '../utils/type-utils';
|
||||||
|
|
||||||
|
type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||||
|
type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
|
||||||
|
type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The method is **sync** in the main process and **async** in the renderer process.
|
||||||
|
*/
|
||||||
|
export const getActivePlugins
|
||||||
|
= process.type === 'renderer'
|
||||||
|
? async () => ipcRenderer.invoke('get-active-plugins')
|
||||||
|
: () => activePlugins;
|
||||||
|
|
||||||
|
if (process.type === 'browser') {
|
||||||
|
ipcMain.handle('get-active-plugins', getActivePlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The method is **sync** in the main process and **async** in the renderer process.
|
||||||
|
*/
|
||||||
|
export const isActive
|
||||||
|
= process.type === 'renderer'
|
||||||
|
? async (plugin: string) =>
|
||||||
|
plugin in (await ipcRenderer.invoke('get-active-plugins'))
|
||||||
|
: (plugin: string): boolean => plugin in activePlugins;
|
||||||
|
|
||||||
|
interface PluginConfigOptions {
|
||||||
|
enableFront: boolean;
|
||||||
|
initialOptions?: OneOfDefaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create a dynamic synced config for plugins.
|
||||||
|
*
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The methods are **sync** in the main process and **async** in the renderer process.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the plugin.
|
||||||
|
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
|
||||||
|
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { PluginConfig } = require("../../config/dynamic");
|
||||||
|
* const config = new PluginConfig("plugin-name", { enableFront: true });
|
||||||
|
* module.exports = { ...config };
|
||||||
|
*
|
||||||
|
* // or
|
||||||
|
*
|
||||||
|
* module.exports = (win, options) => {
|
||||||
|
* const config = new PluginConfig("plugin-name", {
|
||||||
|
* enableFront: true,
|
||||||
|
* initialOptions: options,
|
||||||
|
* });
|
||||||
|
* setupMyPlugin(win, config);
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
type ConfigType<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
|
||||||
|
type ValueOf<T> = T[keyof T];
|
||||||
|
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||||
|
private name: string;
|
||||||
|
private config: ConfigType<T>;
|
||||||
|
private defaultConfig: ConfigType<T>;
|
||||||
|
private enableFront: boolean;
|
||||||
|
|
||||||
|
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
|
||||||
|
private allSubscribers: ((config: ConfigType<T>) => void)[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
name: T,
|
||||||
|
options: PluginConfigOptions = {
|
||||||
|
enableFront: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const pluginDefaultConfig = defaultConfig.plugins[name] ?? {};
|
||||||
|
const pluginConfig = options.initialOptions || getOptions(name) || {};
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.enableFront = options.enableFront;
|
||||||
|
this.defaultConfig = pluginDefaultConfig;
|
||||||
|
this.config = { ...pluginDefaultConfig, ...pluginConfig };
|
||||||
|
|
||||||
|
if (this.enableFront) {
|
||||||
|
this.#setupFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
activePlugins[name] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: keyof ConfigType<T>): Promise<ValueOf<ConfigType<T>>> {
|
||||||
|
return this.config[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
|
||||||
|
this.config[key] = value;
|
||||||
|
this.#onChange(key);
|
||||||
|
this.#save();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
setAll(options: ConfigType<T>) {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new Error('Options must be an object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const [key, value] of Object.entries(options) as Entries<typeof options>) {
|
||||||
|
if (this.config[key] !== value) {
|
||||||
|
this.config[key] = value;
|
||||||
|
this.#onChange(key, false);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
for (const fn of this.allSubscribers) {
|
||||||
|
fn(this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#save();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultConfig() {
|
||||||
|
return this.defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
|
||||||
|
*
|
||||||
|
* Used for options that require a restart to take effect.
|
||||||
|
*/
|
||||||
|
setAndMaybeRestart(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
|
||||||
|
this.config[key] = value;
|
||||||
|
setMenuOptions(this.name, this.config);
|
||||||
|
this.#onChange(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(valueName: keyof ConfigType<T>, fn: (config: ConfigType<T>) => void) {
|
||||||
|
this.subscribers[valueName] = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeAll(fn: (config: ConfigType<T>) => void) {
|
||||||
|
this.allSubscribers.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called only from back */
|
||||||
|
#save() {
|
||||||
|
setOptions(this.name, this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#onChange(valueName: keyof ConfigType<T>, single: boolean = true) {
|
||||||
|
this.subscribers[valueName]?.(this.config[valueName] as ConfigType<T>);
|
||||||
|
if (single) {
|
||||||
|
for (const fn of this.allSubscribers) {
|
||||||
|
fn(this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupFront() {
|
||||||
|
const ignoredMethods = ['subscribe', 'subscribeAll'];
|
||||||
|
|
||||||
|
if (process.type === 'renderer') {
|
||||||
|
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||||
|
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return
|
||||||
|
this[fnName] = (async (...args: any) => await ipcRenderer.invoke(
|
||||||
|
`${this.name}-config-${String(fnName)}`,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
...args,
|
||||||
|
)) as typeof this[keyof this];
|
||||||
|
|
||||||
|
this.subscribe = (valueName, fn: (config: ConfigType<T>) => void) => {
|
||||||
|
if (valueName in this.subscribers) {
|
||||||
|
console.error(`Already subscribed to ${String(valueName)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscribers[valueName] = fn;
|
||||||
|
ipcRenderer.on(
|
||||||
|
`${this.name}-config-changed-${String(valueName)}`,
|
||||||
|
(_, value: ConfigType<T>) => {
|
||||||
|
fn(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.subscribeAll = (fn: (config: ConfigType<T>) => void) => {
|
||||||
|
ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType<T>) => {
|
||||||
|
fn(value);
|
||||||
|
});
|
||||||
|
ipcRenderer.send(`${this.name}-config-subscribe-all`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (process.type === 'browser') {
|
||||||
|
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||||
|
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return
|
||||||
|
ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType<T>) => {
|
||||||
|
this.subscribe(valueName, (value) => {
|
||||||
|
sendToFront(`${this.name}-config-changed-${String(valueName)}`, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on(`${this.name}-config-subscribe-all`, () => {
|
||||||
|
this.subscribeAll((value) => {
|
||||||
|
sendToFront(`${this.name}-config-changed`, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
const defaultConfig = require('./defaults');
|
|
||||||
const plugins = require('./plugins');
|
|
||||||
const store = require('./store');
|
|
||||||
|
|
||||||
const { restart } = require('../providers/app-controls');
|
|
||||||
|
|
||||||
const set = (key, value) => {
|
|
||||||
store.set(key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
function setMenuOption(key, value) {
|
|
||||||
set(key, value);
|
|
||||||
if (store.get('options.restartOnConfigChanges')) {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = (key) => store.get(key);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
defaultConfig,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
setMenuOption,
|
|
||||||
edit: () => store.openInEditor(),
|
|
||||||
watch(cb) {
|
|
||||||
store.onDidChange('options', cb);
|
|
||||||
store.onDidChange('plugins', cb);
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
};
|
|
||||||
57
config/index.ts
Normal file
57
config/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
import defaultConfig from './defaults';
|
||||||
|
import plugins from './plugins';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
import { restart } from '../providers/app-controls';
|
||||||
|
|
||||||
|
|
||||||
|
const set = (key: string, value: unknown) => {
|
||||||
|
store.set(key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setMenuOption(key: string, value: unknown) {
|
||||||
|
set(key, value);
|
||||||
|
if (store.get('options.restartOnConfigChanges')) {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAGIC OF TYPESCRIPT
|
||||||
|
|
||||||
|
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||||
|
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
|
||||||
|
type Join<K, P> = K extends string | number ?
|
||||||
|
P extends string | number ?
|
||||||
|
`${K}${'' extends P ? '' : '.'}${P}`
|
||||||
|
: never : never;
|
||||||
|
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
|
||||||
|
{ [K in keyof T]-?: K extends string | number ?
|
||||||
|
`${K}` | Join<K, Paths<T[K], Prev[D]>>
|
||||||
|
: never
|
||||||
|
}[keyof T] : ''
|
||||||
|
|
||||||
|
type FirstKey<T extends string> = T extends `${infer K}.${string}` ? K : T;
|
||||||
|
type NextKey<T extends string> = T extends `${string}.${infer K}` ? K : T;
|
||||||
|
type PathValue<T, Key extends string> = (
|
||||||
|
T extends object
|
||||||
|
? FirstKey<Key> extends keyof T
|
||||||
|
? PathValue<T[FirstKey<Key>], NextKey<Key>>
|
||||||
|
: T
|
||||||
|
: T
|
||||||
|
);
|
||||||
|
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) => store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
defaultConfig,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
setMenuOption,
|
||||||
|
edit: () => store.openInEditor(),
|
||||||
|
watch(cb: Parameters<Store['onDidChange']>[1]) {
|
||||||
|
store.onDidChange('options', cb);
|
||||||
|
store.onDidChange('plugins', cb);
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
};
|
||||||
@ -1,55 +0,0 @@
|
|||||||
const store = require('./store');
|
|
||||||
|
|
||||||
const { restart } = require('../providers/app-controls');
|
|
||||||
|
|
||||||
function getEnabled() {
|
|
||||||
const plugins = store.get('plugins');
|
|
||||||
return Object.entries(plugins).filter(([plugin]) =>
|
|
||||||
isEnabled(plugin),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEnabled(plugin) {
|
|
||||||
const pluginConfig = store.get('plugins')[plugin];
|
|
||||||
return pluginConfig !== undefined && pluginConfig.enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOptions(plugin, options) {
|
|
||||||
const plugins = store.get('plugins');
|
|
||||||
store.set('plugins', {
|
|
||||||
...plugins,
|
|
||||||
[plugin]: {
|
|
||||||
...plugins[plugin],
|
|
||||||
...options,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMenuOptions(plugin, options) {
|
|
||||||
setOptions(plugin, options);
|
|
||||||
if (store.get('options.restartOnConfigChanges')) {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions(plugin) {
|
|
||||||
return store.get('plugins')[plugin];
|
|
||||||
}
|
|
||||||
|
|
||||||
function enable(plugin) {
|
|
||||||
setMenuOptions(plugin, { enabled: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function disable(plugin) {
|
|
||||||
setMenuOptions(plugin, { enabled: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
isEnabled,
|
|
||||||
getEnabled,
|
|
||||||
enable,
|
|
||||||
disable,
|
|
||||||
setOptions,
|
|
||||||
setMenuOptions,
|
|
||||||
getOptions,
|
|
||||||
};
|
|
||||||
63
config/plugins.ts
Normal file
63
config/plugins.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import store from './store';
|
||||||
|
import defaultConfig from './defaults';
|
||||||
|
|
||||||
|
import { restart } from '../providers/app-controls';
|
||||||
|
import { Entries } from '../utils/type-utils';
|
||||||
|
|
||||||
|
interface Plugin {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||||
|
|
||||||
|
export function getEnabled() {
|
||||||
|
const plugins = store.get('plugins') as DefaultPluginsConfig;
|
||||||
|
return (Object.entries(plugins) as Entries<DefaultPluginsConfig>).filter(([plugin]) =>
|
||||||
|
isEnabled(plugin),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnabled(plugin: string) {
|
||||||
|
const pluginConfig = (store.get('plugins') as Record<string, Plugin>)[plugin];
|
||||||
|
return pluginConfig !== undefined && pluginConfig.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOptions<T>(plugin: string, options: T) {
|
||||||
|
const plugins = store.get('plugins') as Record<string, T>;
|
||||||
|
store.set('plugins', {
|
||||||
|
...plugins,
|
||||||
|
[plugin]: {
|
||||||
|
...plugins[plugin],
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMenuOptions<T>(plugin: string, options: T) {
|
||||||
|
setOptions(plugin, options);
|
||||||
|
if (store.get('options.restartOnConfigChanges')) {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOptions<T>(plugin: string): T {
|
||||||
|
return (store.get('plugins') as Record<string, T>)[plugin];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enable(plugin: string) {
|
||||||
|
setMenuOptions(plugin, { enabled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disable(plugin: string) {
|
||||||
|
setMenuOptions(plugin, { enabled: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isEnabled,
|
||||||
|
getEnabled,
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
setOptions,
|
||||||
|
setMenuOptions,
|
||||||
|
getOptions,
|
||||||
|
};
|
||||||
@ -1,15 +1,16 @@
|
|||||||
const Store = require('electron-store');
|
import Store from 'electron-store';
|
||||||
|
import Conf from 'conf';
|
||||||
|
|
||||||
const defaults = require('./defaults');
|
import defaults from './defaults';
|
||||||
|
|
||||||
const setDefaultPluginOptions = (store, plugin) => {
|
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
|
||||||
if (!store.get(`plugins.${plugin}`)) {
|
if (!store.get(`plugins.${plugin}`)) {
|
||||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const migrations = {
|
const migrations = {
|
||||||
'>=1.20.0'(store) {
|
'>=1.20.0'(store: Conf<Record<string, unknown>>) {
|
||||||
setDefaultPluginOptions(store, 'visualizer');
|
setDefaultPluginOptions(store, 'visualizer');
|
||||||
|
|
||||||
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
||||||
@ -25,14 +26,14 @@ const migrations = {
|
|||||||
store.set('options.likeButtons', 'force');
|
store.set('options.likeButtons', 'force');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.17.0'(store) {
|
'>=1.17.0'(store: Conf<Record<string, unknown>>) {
|
||||||
setDefaultPluginOptions(store, 'picture-in-picture');
|
setDefaultPluginOptions(store, 'picture-in-picture');
|
||||||
|
|
||||||
if (store.get('plugins.video-toggle.mode') === undefined) {
|
if (store.get('plugins.video-toggle.mode') === undefined) {
|
||||||
store.set('plugins.video-toggle.mode', 'custom');
|
store.set('plugins.video-toggle.mode', 'custom');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.14.0'(store) {
|
'>=1.14.0'(store: Conf<Record<string, unknown>>) {
|
||||||
if (
|
if (
|
||||||
typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
|
typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
|
||||||
) {
|
) {
|
||||||
@ -44,18 +45,25 @@ const migrations = {
|
|||||||
store.set('plugins.video-toggle.enabled', true);
|
store.set('plugins.video-toggle.enabled', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.13.0'(store) {
|
'>=1.13.0'(store: Conf<Record<string, unknown>>) {
|
||||||
if (store.get('plugins.discord.listenAlong') === undefined) {
|
if (store.get('plugins.discord.listenAlong') === undefined) {
|
||||||
store.set('plugins.discord.listenAlong', true);
|
store.set('plugins.discord.listenAlong', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.12.0'(store) {
|
'>=1.12.0'(store: Conf<Record<string, unknown>>) {
|
||||||
const options = store.get('plugins.shortcuts');
|
const options = store.get('plugins.shortcuts') as Record<string, {
|
||||||
|
action: string;
|
||||||
|
shortcut: unknown;
|
||||||
|
}[] | Record<string, unknown>>;
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const optionType of ['global', 'local']) {
|
for (const optionType of ['global', 'local']) {
|
||||||
if (Array.isArray(options[optionType])) {
|
if (Array.isArray(options[optionType])) {
|
||||||
const updatedOptions = {};
|
const optionsArray = options[optionType] as {
|
||||||
for (const optionObject of options[optionType]) {
|
action: string;
|
||||||
|
shortcut: unknown;
|
||||||
|
}[];
|
||||||
|
const updatedOptions: Record<string, unknown> = {};
|
||||||
|
for (const optionObject of optionsArray) {
|
||||||
if (optionObject.action && optionObject.shortcut) {
|
if (optionObject.action && optionObject.shortcut) {
|
||||||
updatedOptions[optionObject.action] = optionObject.shortcut;
|
updatedOptions[optionObject.action] = optionObject.shortcut;
|
||||||
}
|
}
|
||||||
@ -70,20 +78,21 @@ const migrations = {
|
|||||||
store.set('plugins.shortcuts', options);
|
store.set('plugins.shortcuts', options);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.11.0'(store) {
|
'>=1.11.0'(store: Conf<Record<string, unknown>>) {
|
||||||
if (store.get('options.resumeOnStart') === undefined) {
|
if (store.get('options.resumeOnStart') === undefined) {
|
||||||
store.set('options.resumeOnStart', true);
|
store.set('options.resumeOnStart', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.7.0'(store) {
|
'>=1.7.0'(store: Conf<Record<string, unknown>>) {
|
||||||
const enabledPlugins = store.get('plugins');
|
const enabledPlugins = store.get('plugins') as string[];
|
||||||
if (!Array.isArray(enabledPlugins)) {
|
if (!Array.isArray(enabledPlugins)) {
|
||||||
console.warn('Plugins are not in array format, cannot migrate');
|
console.warn('Plugins are not in array format, cannot migrate');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include custom options
|
// Include custom options
|
||||||
const plugins = {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const plugins: Record<string, any> = {
|
||||||
adblocker: {
|
adblocker: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
@ -95,7 +104,9 @@ const migrations = {
|
|||||||
downloadFolder: undefined, // Custom download folder (absolute path)
|
downloadFolder: undefined, // Custom download folder (absolute path)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const enabledPlugin of enabledPlugins) {
|
for (const enabledPlugin of enabledPlugins) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
plugins[enabledPlugin] = {
|
plugins[enabledPlugin] = {
|
||||||
...plugins[enabledPlugin],
|
...plugins[enabledPlugin],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -106,7 +117,7 @@ const migrations = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = new Store({
|
export default new Store({
|
||||||
defaults,
|
defaults,
|
||||||
clearInvalidConfig: false,
|
clearInvalidConfig: false,
|
||||||
migrations,
|
migrations,
|
||||||
46
custom-electron-prompt.d.ts
vendored
Normal file
46
custom-electron-prompt.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
declare module 'custom-electron-prompt' {
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
export interface PromptCounterOptions {
|
||||||
|
minimum?: number;
|
||||||
|
maximum?: number;
|
||||||
|
multiFire?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptKeybindOptions {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizable?: boolean;
|
||||||
|
title?: string;
|
||||||
|
label?: string;
|
||||||
|
buttonLabels?: {
|
||||||
|
ok?: string;
|
||||||
|
cancel?: string;
|
||||||
|
};
|
||||||
|
alwaysOnTop?: boolean;
|
||||||
|
value?: string;
|
||||||
|
type?: 'input' | 'select' | 'counter';
|
||||||
|
selectOptions?: Record<string, string>;
|
||||||
|
keybindOptions?: PromptKeybindOptions[];
|
||||||
|
counterOptions?: PromptCounterOptions;
|
||||||
|
icon?: string;
|
||||||
|
useHtmlLabel?: boolean;
|
||||||
|
customStylesheet?: string;
|
||||||
|
menuBarVisible?: boolean;
|
||||||
|
skipTaskbar?: boolean;
|
||||||
|
frame?: boolean;
|
||||||
|
customScript?: string;
|
||||||
|
enableRemoteModule?: boolean;
|
||||||
|
inputAttrs: Partial<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt: (options?: PromptOptions, parent?: BrowserWindow) => Promise<string | null>;
|
||||||
|
|
||||||
|
export default prompt;
|
||||||
|
}
|
||||||
@ -1,20 +1,23 @@
|
|||||||
'use strict';
|
import path from 'node:path';
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const electron = require('electron');
|
import electron, { BrowserWindow } from 'electron';
|
||||||
const enhanceWebRequest = require('electron-better-web-request').default;
|
import enhanceWebRequest from 'electron-better-web-request';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
const unhandled = require('electron-unhandled');
|
import unhandled from 'electron-unhandled';
|
||||||
const { autoUpdater } = require('electron-updater');
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import electronDebug from 'electron-debug';
|
||||||
|
|
||||||
|
import { BetterWebRequest } from 'electron-better-web-request/lib/electron-better-web-request';
|
||||||
|
|
||||||
|
import config from './config';
|
||||||
|
import { setApplicationMenu } from './menu';
|
||||||
|
import { fileExists, injectCSS } from './plugins/utils';
|
||||||
|
import { isTesting } from './utils/testing';
|
||||||
|
import { setUpTray } from './tray';
|
||||||
|
import { setupSongInfo } from './providers/song-info';
|
||||||
|
import { restart, setupAppControls } from './providers/app-controls';
|
||||||
|
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
const { setApplicationMenu } = require('./menu');
|
|
||||||
const { fileExists, injectCSS } = require('./plugins/utils');
|
|
||||||
const { isTesting } = require('./utils/testing');
|
|
||||||
const { setUpTray } = require('./tray');
|
|
||||||
const { setupSongInfo } = require('./providers/song-info');
|
|
||||||
const { setupAppControls, restart } = require('./providers/app-controls');
|
|
||||||
const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require('./providers/protocol-handler');
|
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -27,7 +30,7 @@ process.env.NODE_OPTIONS = '';
|
|||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
// Prevent window being garbage collected
|
// Prevent window being garbage collected
|
||||||
let mainWindow;
|
let mainWindow: Electron.BrowserWindow | null;
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
@ -45,7 +48,7 @@ if (config.get('options.disableHardwareAcceleration')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is.linux() && config.plugins.isEnabled('shortcuts')) {
|
if (is.linux() && config.plugins.isEnabled('shortcuts')) {
|
||||||
// Stops chromium from launching it's own mpris service
|
// Stops chromium from launching its own MPRIS service
|
||||||
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +57,7 @@ if (config.get('options.proxy')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||||
require('electron-debug')({
|
electronDebug({
|
||||||
showDevTools: false, // Disable automatic devTools on new window
|
showDevTools: false, // Disable automatic devTools on new window
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,15 +70,14 @@ if (process.platform === 'win32') {
|
|||||||
|
|
||||||
function onClosed() {
|
function onClosed() {
|
||||||
// Dereference the window
|
// Dereference the window
|
||||||
// For multiple windows store them in an array
|
// For multiple Windows store them in an array
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Electron.BrowserWindow} win */
|
function loadPlugins(win: BrowserWindow) {
|
||||||
function loadPlugins(win) {
|
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
|
injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
|
||||||
// Load user CSS
|
// Load user CSS
|
||||||
const themes = config.get('options.themes');
|
const themes: string[] = config.get('options.themes');
|
||||||
if (Array.isArray(themes)) {
|
if (Array.isArray(themes)) {
|
||||||
for (const cssFile of themes) {
|
for (const cssFile of themes) {
|
||||||
fileExists(
|
fileExists(
|
||||||
@ -101,7 +103,8 @@ function loadPlugins(win) {
|
|||||||
console.log('Loaded plugin - ' + plugin);
|
console.log('Loaded plugin - ' + plugin);
|
||||||
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
|
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
|
||||||
fileExists(pluginPath, () => {
|
fileExists(pluginPath, () => {
|
||||||
const handle = require(pluginPath);
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const handle = require(pluginPath) as (window: BrowserWindow, option: typeof options) => void;
|
||||||
handle(win, options);
|
handle(win, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,7 +113,7 @@ function loadPlugins(win) {
|
|||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
const windowSize = config.get('window-size');
|
const windowSize = config.get('window-size');
|
||||||
const windowMaximized = config.get('window-maximized');
|
const windowMaximized = config.get('window-maximized');
|
||||||
const windowPosition = config.get('window-position');
|
const windowPosition: Electron.Point = config.get('window-position');
|
||||||
const useInlineMenu = config.plugins.isEnabled('in-app-menu');
|
const useInlineMenu = config.plugins.isEnabled('in-app-menu');
|
||||||
|
|
||||||
const win = new electron.BrowserWindow({
|
const win = new electron.BrowserWindow({
|
||||||
@ -120,12 +123,11 @@ function createMainWindow() {
|
|||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm
|
// TODO: re-enable contextIsolation once it can work with FFMpeg.wasm
|
||||||
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
|
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
affinity: 'main-window', // Main window, and addition windows should work in one process
|
|
||||||
...(isTesting()
|
...(isTesting()
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
@ -158,7 +160,7 @@ function createMainWindow() {
|
|||||||
// Window is offscreen
|
// Window is offscreen
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
console.log(
|
||||||
`Window tried to render offscreen, windowSize=${winSize}, displaySize=${displaySize}, position=${windowPosition}`,
|
`Window tried to render offscreen, windowSize=${String(winSize)}, displaySize=${String(displaySize)}, position=${String(windowPosition)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -180,10 +182,12 @@ function createMainWindow() {
|
|||||||
win.webContents.loadURL(urlToLoad);
|
win.webContents.loadURL(urlToLoad);
|
||||||
win.on('closed', onClosed);
|
win.on('closed', onClosed);
|
||||||
|
|
||||||
|
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
|
||||||
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
||||||
? (key, value) => require('./plugins/picture-in-picture/back').setOptions({ [key]: value })
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
: () => {
|
? (key: string, value: unknown) => (require('./plugins/picture-in-picture/back') as typeof import('./plugins/picture-in-picture/back'))
|
||||||
};
|
.setOptions({ [key]: value })
|
||||||
|
: () => {};
|
||||||
|
|
||||||
win.on('move', () => {
|
win.on('move', () => {
|
||||||
if (win.isMaximized()) {
|
if (win.isMaximized()) {
|
||||||
@ -191,17 +195,18 @@ function createMainWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const position = win.getPosition();
|
const position = win.getPosition();
|
||||||
const isPiPEnabled
|
const isPiPEnabled: boolean
|
||||||
= config.plugins.isEnabled('picture-in-picture')
|
= config.plugins.isEnabled('picture-in-picture')
|
||||||
&& config.plugins.getOptions('picture-in-picture').isInPiP;
|
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
|
||||||
if (!isPiPEnabled) {
|
if (!isPiPEnabled) {
|
||||||
|
|
||||||
lateSave('window-position', { x: position[0], y: position[1] });
|
lateSave('window-position', { x: position[0], y: position[1] });
|
||||||
} else if (config.plugins.getOptions('picture-in-picture').savePosition) {
|
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').savePosition) {
|
||||||
lateSave('pip-position', position, setPiPOptions);
|
lateSave('pip-position', position, setPiPOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let winWasMaximized;
|
let winWasMaximized: boolean;
|
||||||
|
|
||||||
win.on('resize', () => {
|
win.on('resize', () => {
|
||||||
const windowSize = win.getSize();
|
const windowSize = win.getSize();
|
||||||
@ -209,7 +214,7 @@ function createMainWindow() {
|
|||||||
|
|
||||||
const isPiPEnabled
|
const isPiPEnabled
|
||||||
= config.plugins.isEnabled('picture-in-picture')
|
= config.plugins.isEnabled('picture-in-picture')
|
||||||
&& config.plugins.getOptions('picture-in-picture').isInPiP;
|
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
|
||||||
|
|
||||||
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
||||||
winWasMaximized = isMaximized;
|
winWasMaximized = isMaximized;
|
||||||
@ -225,14 +230,14 @@ function createMainWindow() {
|
|||||||
width: windowSize[0],
|
width: windowSize[0],
|
||||||
height: windowSize[1],
|
height: windowSize[1],
|
||||||
});
|
});
|
||||||
} else if (config.plugins.getOptions('picture-in-picture').saveSize) {
|
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').saveSize) {
|
||||||
lateSave('pip-size', windowSize, setPiPOptions);
|
lateSave('pip-size', windowSize, setPiPOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedTimeouts = {};
|
const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {};
|
||||||
|
|
||||||
function lateSave(key, value, fn = config.set) {
|
function lateSave(key: string, value: unknown, fn: (key: string, value: unknown) => void = config.set) {
|
||||||
if (savedTimeouts[key]) {
|
if (savedTimeouts[key]) {
|
||||||
clearTimeout(savedTimeouts[key]);
|
clearTimeout(savedTimeouts[key]);
|
||||||
}
|
}
|
||||||
@ -243,7 +248,7 @@ function createMainWindow() {
|
|||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
win.webContents.on('render-process-gone', (event, webContents, details) => {
|
app.on('render-process-gone', (event, webContents, details) => {
|
||||||
showUnresponsiveDialog(win, details);
|
showUnresponsiveDialog(win, details);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -434,7 +439,7 @@ app.on('ready', () => {
|
|||||||
autoUpdater.on('update-available', () => {
|
autoUpdater.on('update-available', () => {
|
||||||
const downloadLink
|
const downloadLink
|
||||||
= 'https://github.com/th-ch/youtube-music/releases/latest';
|
= 'https://github.com/th-ch/youtube-music/releases/latest';
|
||||||
const dialogOptions = {
|
const dialogOptions: Electron.MessageBoxOptions = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK', 'Download', 'Disable updates'],
|
buttons: ['OK', 'Download', 'Disable updates'],
|
||||||
title: 'Application Update',
|
title: 'Application Update',
|
||||||
@ -486,13 +491,13 @@ app.on('ready', () => {
|
|||||||
// Hide the window instead of quitting (quit is available in tray options)
|
// Hide the window instead of quitting (quit is available in tray options)
|
||||||
if (!forceQuit) {
|
if (!forceQuit) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow!.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function showUnresponsiveDialog(win, details) {
|
function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProcessGoneDetails) {
|
||||||
if (details) {
|
if (details) {
|
||||||
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
||||||
}
|
}
|
||||||
@ -501,7 +506,7 @@ function showUnresponsiveDialog(win, details) {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Window Unresponsive',
|
title: 'Window Unresponsive',
|
||||||
message: 'The Application is Unresponsive',
|
message: 'The Application is Unresponsive',
|
||||||
details: 'We are sorry for the inconvenience! please choose what to do:',
|
detail: 'We are sorry for the inconvenience! please choose what to do:',
|
||||||
buttons: ['Wait', 'Relaunch', 'Quit'],
|
buttons: ['Wait', 'Relaunch', 'Quit'],
|
||||||
cancelId: 0,
|
cancelId: 0,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
@ -519,8 +524,10 @@ function showUnresponsiveDialog(win, details) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: electron-better-web-request's typing is wrong
|
||||||
|
type BetterSession = Omit<Electron.Session, 'webRequest'> & { webRequest: BetterWebRequest & Electron.WebRequest };
|
||||||
function removeContentSecurityPolicy(
|
function removeContentSecurityPolicy(
|
||||||
session = electron.session.defaultSession,
|
session: BetterSession = electron.session.defaultSession as BetterSession,
|
||||||
) {
|
) {
|
||||||
// Allows defining multiple "onHeadersReceived" listeners
|
// Allows defining multiple "onHeadersReceived" listeners
|
||||||
// by enhancing the session.
|
// by enhancing the session.
|
||||||
@ -538,15 +545,16 @@ function removeContentSecurityPolicy(
|
|||||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ResolverListener = { apply: () => Record<string, unknown>; context: unknown };
|
||||||
// When multiple listeners are defined, apply them all
|
// When multiple listeners are defined, apply them all
|
||||||
session.webRequest.setResolver('onHeadersReceived', (listeners) => {
|
session.webRequest.setResolver('onHeadersReceived', (listeners: ResolverListener[]) => {
|
||||||
return listeners.reduce(
|
return listeners.reduce(
|
||||||
async (accumulator, listener) => {
|
(accumulator: Record<string, unknown>, listener: ResolverListener) => {
|
||||||
if (accumulator.cancel) {
|
if (accumulator.cancel) {
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await listener.apply();
|
const result = listener.apply();
|
||||||
return { ...accumulator, ...result };
|
return { ...accumulator, ...result };
|
||||||
},
|
},
|
||||||
{ cancel: false },
|
{ cancel: false },
|
||||||
@ -1,24 +1,26 @@
|
|||||||
const { existsSync } = require('node:fs');
|
import { existsSync } from 'node:fs';
|
||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const { app, clipboard, Menu, dialog } = require('electron');
|
import is from 'electron-is';
|
||||||
const is = require('electron-is');
|
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
||||||
const prompt = require('custom-electron-prompt');
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
const { restart } = require('./providers/app-controls');
|
import { restart } from './providers/app-controls';
|
||||||
const { getAllPlugins } = require('./plugins/utils');
|
import { getAllPlugins } from './plugins/utils';
|
||||||
const config = require('./config');
|
import config from './config';
|
||||||
const { startingPages } = require('./providers/extracted-data');
|
import { startingPages } from './providers/extracted-data';
|
||||||
const promptOptions = require('./providers/prompt-options');
|
import promptOptions from './providers/prompt-options';
|
||||||
|
|
||||||
|
export type MenuTemplate = (Electron.MenuItemConstructorOptions | Electron.MenuItem)[];
|
||||||
|
|
||||||
// True only if in-app-menu was loaded on launch
|
// True only if in-app-menu was loaded on launch
|
||||||
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
||||||
|
|
||||||
const pluginEnabledMenu = (plugin, label = '', hasSubmenu = false, refreshMenu = undefined) => ({
|
const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refreshMenu: (() => void ) | undefined = undefined): Electron.MenuItemConstructorOptions => ({
|
||||||
label: label || plugin,
|
label: label || plugin,
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.plugins.isEnabled(plugin),
|
checked: config.plugins.isEnabled(plugin),
|
||||||
click(item) {
|
click(item: Electron.MenuItem) {
|
||||||
if (item.checked) {
|
if (item.checked) {
|
||||||
config.plugins.enable(plugin);
|
config.plugins.enable(plugin);
|
||||||
} else {
|
} else {
|
||||||
@ -26,14 +28,14 @@ const pluginEnabledMenu = (plugin, label = '', hasSubmenu = false, refreshMenu =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasSubmenu) {
|
if (hasSubmenu) {
|
||||||
refreshMenu();
|
refreshMenu?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainMenuTemplate = (win) => {
|
export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||||
const refreshMenu = () => {
|
const refreshMenu = () => {
|
||||||
this.setApplicationMenu(win);
|
setApplicationMenu(win);
|
||||||
if (inAppMenuActive) {
|
if (inAppMenuActive) {
|
||||||
win.webContents.send('refreshMenu');
|
win.webContents.send('refreshMenu');
|
||||||
}
|
}
|
||||||
@ -55,7 +57,9 @@ const mainMenuTemplate = (win) => {
|
|||||||
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
|
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPluginMenu = require(pluginPath);
|
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;
|
||||||
return {
|
return {
|
||||||
label: pluginLabel,
|
label: pluginLabel,
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -63,12 +67,11 @@ const mainMenuTemplate = (win) => {
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||||
],
|
],
|
||||||
};
|
} satisfies Electron.MenuItemConstructorOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pluginEnabledMenu(plugin);
|
return pluginEnabledMenu(plugin);
|
||||||
})
|
}),
|
||||||
,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Options',
|
label: 'Options',
|
||||||
@ -208,7 +211,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||||
...(is.windows() || is.macOS()
|
...(is.windows() || is.macOS()
|
||||||
? // Only works on Win/Mac
|
? // Only works on Win/Mac
|
||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||||
@ -222,7 +225,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||||
{
|
{
|
||||||
label: 'Tray',
|
label: 'Tray',
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -238,8 +241,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
{
|
{
|
||||||
label: 'Enabled + app visible',
|
label: 'Enabled + app visible',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked:
|
checked: !!(config.get('options.tray') && config.get('options.appVisible')),
|
||||||
config.get('options.tray') && config.get('options.appVisible'),
|
|
||||||
click() {
|
click() {
|
||||||
config.setMenuOption('options.tray', true);
|
config.setMenuOption('options.tray', true);
|
||||||
config.setMenuOption('options.appVisible', true);
|
config.setMenuOption('options.appVisible', true);
|
||||||
@ -248,8 +250,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
{
|
{
|
||||||
label: 'Enabled + app hidden',
|
label: 'Enabled + app hidden',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked:
|
checked: !!(config.get('options.tray') && !config.get('options.appVisible')),
|
||||||
config.get('options.tray') && !config.get('options.appVisible'),
|
|
||||||
click() {
|
click() {
|
||||||
config.setMenuOption('options.tray', true);
|
config.setMenuOption('options.tray', true);
|
||||||
config.setMenuOption('options.appVisible', false);
|
config.setMenuOption('options.appVisible', false);
|
||||||
@ -320,8 +321,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
if (webContents.isDevToolsOpened()) {
|
if (webContents.isDevToolsOpened()) {
|
||||||
webContents.closeDevTools();
|
webContents.closeDevTools();
|
||||||
} else {
|
} else {
|
||||||
const devToolsOptions = {};
|
webContents.openDevTools();
|
||||||
webContents.openDevTools(devToolsOptions);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -384,10 +384,8 @@ const mainMenuTemplate = (win) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||||
module.exports.mainMenuTemplate = mainMenuTemplate;
|
const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
|
||||||
module.exports.setApplicationMenu = (win) => {
|
|
||||||
const menuTemplate = [...mainMenuTemplate(win)];
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const { name } = app;
|
const { name } = app;
|
||||||
menuTemplate.unshift({
|
menuTemplate.unshift({
|
||||||
@ -396,17 +394,13 @@ module.exports.setApplicationMenu = (win) => {
|
|||||||
{ role: 'about' },
|
{ role: 'about' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'hide' },
|
{ role: 'hide' },
|
||||||
{ role: 'hideothers' },
|
{ role: 'hideOthers' },
|
||||||
{ role: 'unhide' },
|
{ role: 'unhide' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{ role: 'selectAll' },
|
||||||
label: 'Select All',
|
{ role: 'cut' },
|
||||||
accelerator: 'CmdOrCtrl+A',
|
{ role: 'copy' },
|
||||||
selector: 'selectAll:',
|
{ role: 'paste' },
|
||||||
},
|
|
||||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
|
|
||||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
|
|
||||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
|
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'minimize' },
|
{ role: 'minimize' },
|
||||||
{ role: 'close' },
|
{ role: 'close' },
|
||||||
@ -419,7 +413,7 @@ module.exports.setApplicationMenu = (win) => {
|
|||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function setProxy(item, win) {
|
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
||||||
const output = await prompt({
|
const output = await prompt({
|
||||||
title: 'Set Proxy',
|
title: 'Set Proxy',
|
||||||
label: 'Enter Proxy Address: (leave empty to disable)',
|
label: 'Enter Proxy Address: (leave empty to disable)',
|
||||||
324
package-lock.json
generated
324
package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"browser-id3-writer": "5.0.0",
|
"browser-id3-writer": "5.0.0",
|
||||||
"butterchurn": "2.6.7",
|
"butterchurn": "2.6.7",
|
||||||
"butterchurn-presets": "2.4.7",
|
"butterchurn-presets": "2.4.7",
|
||||||
|
"conf": "10.2.0",
|
||||||
"custom-electron-prompt": "1.5.7",
|
"custom-electron-prompt": "1.5.7",
|
||||||
"custom-electron-titlebar": "4.1.6",
|
"custom-electron-titlebar": "4.1.6",
|
||||||
"electron-better-web-request": "1.0.1",
|
"electron-better-web-request": "1.0.1",
|
||||||
@ -42,6 +43,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/youtube-player": "^5.5.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
"del-cli": "5.0.1",
|
"del-cli": "5.0.1",
|
||||||
"electron": "27.0.0-alpha.5",
|
"electron": "27.0.0-alpha.5",
|
||||||
@ -52,7 +56,8 @@
|
|||||||
"eslint-plugin-prettier": "5.0.0",
|
"eslint-plugin-prettier": "5.0.0",
|
||||||
"node-gyp": "9.4.0",
|
"node-gyp": "9.4.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"playwright": "1.37.1"
|
"playwright": "1.37.1",
|
||||||
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@ -1128,6 +1133,12 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@total-typescript/ts-reset": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/cacheable-request": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
@ -1194,6 +1205,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||||
|
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
@ -1250,6 +1267,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/semver": {
|
||||||
|
"version": "7.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz",
|
||||||
|
"integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.6",
|
"version": "1.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
|
||||||
@ -1266,6 +1289,231 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/youtube-player": {
|
||||||
|
"version": "5.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.7.tgz",
|
||||||
|
"integrity": "sha512-W8F4eoTIvzXeNrT3JroQPimZLXnlJA8smYygHZUKFPVoYwgs/OhJkA1VBhL3iSs57OQkuINqHlY4SmMT5wtnJg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
|
"@typescript-eslint/scope-manager": "6.5.0",
|
||||||
|
"@typescript-eslint/type-utils": "6.5.0",
|
||||||
|
"@typescript-eslint/utils": "6.5.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.5.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"graphemer": "^1.4.0",
|
||||||
|
"ignore": "^5.2.4",
|
||||||
|
"natural-compare": "^1.4.0",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"ts-api-utils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||||
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/parser": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/scope-manager": "6.5.0",
|
||||||
|
"@typescript-eslint/types": "6.5.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "6.5.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.5.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "6.5.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-f7OcZOkRivtujIBQ4yrJNIuwyCQO1OjocVqntl9dgSIZAdKqicj3xFDqDOzHDlGCZX990LqhLQXWRnQvsapq8A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/typescript-estree": "6.5.0",
|
||||||
|
"@typescript-eslint/utils": "6.5.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"ts-api-utils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/types": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "6.5.0",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.5.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"globby": "^11.1.0",
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"ts-api-utils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/globby": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"array-union": "^2.1.0",
|
||||||
|
"dir-glob": "^3.0.1",
|
||||||
|
"fast-glob": "^3.2.9",
|
||||||
|
"ignore": "^5.2.0",
|
||||||
|
"merge2": "^1.4.1",
|
||||||
|
"slash": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/utils": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
|
"@types/json-schema": "^7.0.12",
|
||||||
|
"@types/semver": "^7.5.0",
|
||||||
|
"@typescript-eslint/scope-manager": "6.5.0",
|
||||||
|
"@typescript-eslint/types": "6.5.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "6.5.0",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "6.5.0",
|
||||||
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xhayper/discord-rpc": {
|
"node_modules/@xhayper/discord-rpc": {
|
||||||
"version": "1.0.22",
|
"version": "1.0.22",
|
||||||
"resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.0.22.tgz",
|
||||||
@ -1631,6 +1879,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/array-union": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array.prototype.findlastindex": {
|
"node_modules/array.prototype.findlastindex": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
|
||||||
@ -2526,6 +2783,19 @@
|
|||||||
"typescript": "^4.0.2"
|
"typescript": "^4.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/config-file-ts/node_modules/typescript": {
|
||||||
|
"version": "4.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/console-control-strings": {
|
"node_modules/console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
@ -2624,6 +2894,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/debounce-fn/node_modules/mimic-fn": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@ -3977,18 +4255,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/execa/node_modules/mimic-fn": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/execa/node_modules/onetime": {
|
"node_modules/execa/node_modules/onetime": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||||
@ -5966,11 +6232,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-fn": {
|
"node_modules/mimic-fn": {
|
||||||
"version": "3.1.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
|
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
@ -8271,6 +8541,18 @@
|
|||||||
"utf8-byte-length": "^1.0.1"
|
"utf8-byte-length": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-api-utils": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||||
@ -8397,16 +8679,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
|
|||||||
23
package.json
23
package.json
@ -76,17 +76,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"output": "./pack/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:debug": "DEBUG=pw:browser* playwright test",
|
"test:debug": "DEBUG=pw:browser* playwright test",
|
||||||
"start": "electron .",
|
"start": "tsc && electron ./dist/index.js",
|
||||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
"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",
|
"clean": "del-cli dist && del-cli pack",
|
||||||
"build": "npm run clean && electron-builder --win --mac --linux -p never",
|
"build": "npm run clean && tsc && electron-builder --win --mac --linux -p never",
|
||||||
"build:linux": "npm run clean && electron-builder --linux -p never",
|
"build:linux": "npm run clean && electron-builder --linux -p never",
|
||||||
"build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never",
|
"build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never",
|
||||||
"build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never",
|
"build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never",
|
||||||
@ -95,11 +98,12 @@
|
|||||||
"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 && node plugins/adblocker/blocker.js",
|
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.ts",
|
||||||
"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",
|
||||||
"release:win": "npm run clean && electron-builder --win -p always"
|
"release:win": "npm run clean && electron-builder --win -p always",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@ -114,6 +118,7 @@
|
|||||||
"browser-id3-writer": "5.0.0",
|
"browser-id3-writer": "5.0.0",
|
||||||
"butterchurn": "2.6.7",
|
"butterchurn": "2.6.7",
|
||||||
"butterchurn-presets": "2.4.7",
|
"butterchurn-presets": "2.4.7",
|
||||||
|
"conf": "10.2.0",
|
||||||
"custom-electron-prompt": "1.5.7",
|
"custom-electron-prompt": "1.5.7",
|
||||||
"custom-electron-titlebar": "4.1.6",
|
"custom-electron-titlebar": "4.1.6",
|
||||||
"electron-better-web-request": "1.0.1",
|
"electron-better-web-request": "1.0.1",
|
||||||
@ -143,6 +148,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/youtube-player": "^5.5.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
"del-cli": "5.0.1",
|
"del-cli": "5.0.1",
|
||||||
"electron": "27.0.0-alpha.5",
|
"electron": "27.0.0-alpha.5",
|
||||||
@ -153,7 +161,8 @@
|
|||||||
"eslint-plugin-prettier": "5.0.0",
|
"eslint-plugin-prettier": "5.0.0",
|
||||||
"node-gyp": "9.4.0",
|
"node-gyp": "9.4.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"playwright": "1.37.1"
|
"playwright": "1.37.1",
|
||||||
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"auto-changelog": {
|
"auto-changelog": {
|
||||||
"hideCredit": true,
|
"hideCredit": true,
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
const { loadAdBlockerEngine } = require('./blocker');
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
module.exports = async (win, options) => {
|
|
||||||
if (await config.shouldUseBlocklists()) {
|
|
||||||
loadAdBlockerEngine(
|
|
||||||
win.webContents.session,
|
|
||||||
options.cache,
|
|
||||||
options.additionalBlockLists,
|
|
||||||
options.disableDefaultLists,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
20
plugins/adblocker/back.ts
Normal file
20
plugins/adblocker/back.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
import { loadAdBlockerEngine } from './blocker';
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
import pluginConfig from '../../config';
|
||||||
|
|
||||||
|
const AdBlockOptionsObj = pluginConfig.get('plugins.adblocker');
|
||||||
|
type AdBlockOptions = typeof AdBlockOptionsObj;
|
||||||
|
|
||||||
|
export default async (win: BrowserWindow, options: AdBlockOptions) => {
|
||||||
|
if (await config.shouldUseBlocklists()) {
|
||||||
|
loadAdBlockerEngine(
|
||||||
|
win.webContents.session,
|
||||||
|
options.cache,
|
||||||
|
options.additionalBlockLists,
|
||||||
|
options.disableDefaultLists,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,7 +1,8 @@
|
|||||||
const { promises } = require('node:fs'); // Used for caching
|
// Used for caching
|
||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
import { promises } from 'node:fs';
|
||||||
|
|
||||||
const { ElectronBlocker } = require('@cliqz/adblocker-electron');
|
import { ElectronBlocker } from '@cliqz/adblocker-electron';
|
||||||
|
|
||||||
const SOURCES = [
|
const SOURCES = [
|
||||||
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
|
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
|
||||||
@ -15,11 +16,11 @@ const SOURCES = [
|
|||||||
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
|
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
|
||||||
];
|
];
|
||||||
|
|
||||||
const loadAdBlockerEngine = (
|
export const loadAdBlockerEngine = (
|
||||||
session = undefined,
|
session: Electron.Session | undefined = undefined,
|
||||||
cache = true,
|
cache = true,
|
||||||
additionalBlockLists = [],
|
additionalBlockLists = [],
|
||||||
disableDefaultLists = false,
|
disableDefaultLists: boolean | string[] = false,
|
||||||
) => {
|
) => {
|
||||||
// Only use cache if no additional blocklists are passed
|
// Only use cache if no additional blocklists are passed
|
||||||
const cachingOptions
|
const cachingOptions
|
||||||
@ -56,7 +57,7 @@ const loadAdBlockerEngine = (
|
|||||||
.catch((error) => console.log('Error loading adBlocker engine', error));
|
.catch((error) => console.log('Error loading adBlocker engine', error));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { loadAdBlockerEngine };
|
export default { loadAdBlockerEngine };
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
loadAdBlockerEngine(); // Generate the engine without enabling it
|
loadAdBlockerEngine(); // Generate the engine without enabling it
|
||||||
}
|
}
|
||||||
@ -1,13 +0,0 @@
|
|||||||
const { PluginConfig } = require('../../config/dynamic');
|
|
||||||
|
|
||||||
const config = new PluginConfig('adblocker', { enableFront: true });
|
|
||||||
|
|
||||||
const blockers = {
|
|
||||||
WithBlocklists: 'With blocklists',
|
|
||||||
InPlayer: 'In player',
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldUseBlocklists = async () =>
|
|
||||||
(await config.get('blocker')) !== blockers.InPlayer;
|
|
||||||
|
|
||||||
module.exports = { shouldUseBlocklists, blockers, ...config };
|
|
||||||
17
plugins/adblocker/config.ts
Normal file
17
plugins/adblocker/config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { PluginConfig } from '../../config/dynamic';
|
||||||
|
|
||||||
|
const config = new PluginConfig('adblocker', { enableFront: true });
|
||||||
|
|
||||||
|
export const blockers = {
|
||||||
|
WithBlocklists: 'With blocklists',
|
||||||
|
InPlayer: 'In player',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldUseBlocklists = async () => await config.get('blocker') !== blockers.InPlayer;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
shouldUseBlocklists,
|
||||||
|
blockers,
|
||||||
|
get: config.get.bind(this),
|
||||||
|
set: config.set.bind(this),
|
||||||
|
};
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
// Source: https://addons.mozilla.org/en-US/firefox/addon/adblock-for-youtube/
|
// Source: https://addons.mozilla.org/en-US/firefox/addon/adblock-for-youtube/
|
||||||
// https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F
|
// https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
const config = require('./config');
|
|
||||||
|
|
||||||
module.exports = () => [
|
|
||||||
{
|
|
||||||
label: 'Blocker',
|
|
||||||
submenu: Object.values(config.blockers).map((blocker) => ({
|
|
||||||
label: blocker,
|
|
||||||
type: 'radio',
|
|
||||||
checked: (config.get('blocker') || config.blockers.WithBlocklists) === blocker,
|
|
||||||
click() {
|
|
||||||
config.set('blocker', blocker);
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
19
plugins/adblocker/menu.ts
Normal file
19
plugins/adblocker/menu.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import config from './config';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const blockerConfig = await config.get('blocker');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Blocker',
|
||||||
|
submenu: Object.values(config.blockers).map((blocker) => ({
|
||||||
|
label: blocker,
|
||||||
|
type: 'radio',
|
||||||
|
checked: (blockerConfig || config.blockers.WithBlocklists) === blocker,
|
||||||
|
click() {
|
||||||
|
config.set('blocker', blocker);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -1,10 +1,10 @@
|
|||||||
const config = require('./config');
|
import config from './config';
|
||||||
|
|
||||||
module.exports = async () => {
|
export default async () => {
|
||||||
if (await config.shouldUseBlocklists()) {
|
if (await config.shouldUseBlocklists()) {
|
||||||
// Preload adblocker to inject scripts/styles
|
// Preload adblocker to inject scripts/styles
|
||||||
require('@cliqz/adblocker-electron-preload');
|
require('@cliqz/adblocker-electron-preload');
|
||||||
} else if ((await config.get('blocker')) === config.blockers.InPlayer) {
|
} else if ((await config.get('blocker')) === config.blockers.InPlayer) {
|
||||||
require('./inject');
|
require('./inject.js');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1,24 +0,0 @@
|
|||||||
const { triggerAction } = require('../utils');
|
|
||||||
|
|
||||||
const CHANNEL = 'navigation';
|
|
||||||
const ACTIONS = {
|
|
||||||
NEXT: 'next',
|
|
||||||
BACK: 'back',
|
|
||||||
};
|
|
||||||
|
|
||||||
function goToNextPage() {
|
|
||||||
triggerAction(CHANNEL, ACTIONS.NEXT);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToPreviousPage() {
|
|
||||||
triggerAction(CHANNEL, ACTIONS.BACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
CHANNEL,
|
|
||||||
ACTIONS,
|
|
||||||
actions: {
|
|
||||||
goToNextPage,
|
|
||||||
goToPreviousPage,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
21
plugins/navigation/actions.ts
Normal file
21
plugins/navigation/actions.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Actions, triggerAction } from '../utils';
|
||||||
|
|
||||||
|
export const CHANNEL = 'navigation';
|
||||||
|
export const ACTIONS = Actions;
|
||||||
|
|
||||||
|
export function goToNextPage() {
|
||||||
|
triggerAction(CHANNEL, Actions.NEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goToPreviousPage() {
|
||||||
|
triggerAction(CHANNEL, Actions.BACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
CHANNEL,
|
||||||
|
ACTIONS,
|
||||||
|
actions: {
|
||||||
|
goToNextPage,
|
||||||
|
goToPreviousPage,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const { ACTIONS, CHANNEL } = require('./actions.js');
|
const { ACTIONS, CHANNEL } = require('./actions.ts');
|
||||||
|
|
||||||
const { injectCSS, listenAction } = require('../utils');
|
const { injectCSS, listenAction } = require('../utils');
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,35 @@
|
|||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const { app, ipcMain } = require('electron');
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
const { setOptions } = require('../../config/plugins');
|
import { setOptions as setPluginOptions } from '../../config/plugins';
|
||||||
const { injectCSS } = require('../utils');
|
import { injectCSS } from '../utils';
|
||||||
|
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
let isInPiP = false;
|
let isInPiP = false;
|
||||||
let originalPosition;
|
let originalPosition: number[];
|
||||||
let originalSize;
|
let originalSize: number[];
|
||||||
let originalFullScreen;
|
let originalFullScreen: boolean;
|
||||||
let originalMaximized;
|
let originalMaximized: boolean;
|
||||||
|
|
||||||
let win;
|
let win: BrowserWindow;
|
||||||
let options;
|
|
||||||
|
// Magic of TypeScript
|
||||||
|
const PiPOptionsObj = config.get('plugins.picture-in-picture');
|
||||||
|
type PiPOptions = typeof PiPOptionsObj;
|
||||||
|
|
||||||
|
let options: Partial<PiPOptions>;
|
||||||
|
|
||||||
const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
|
const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
|
||||||
const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
|
const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
|
||||||
|
|
||||||
const setLocalOptions = (_options) => {
|
const setLocalOptions = (_options: Partial<PiPOptions>) => {
|
||||||
options = { ...options, ..._options };
|
options = { ...options, ..._options };
|
||||||
setOptions('picture-in-picture', _options);
|
setPluginOptions('picture-in-picture', _options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePiP = async () => {
|
const togglePiP = () => {
|
||||||
isInPiP = !isInPiP;
|
isInPiP = !isInPiP;
|
||||||
setLocalOptions({ isInPiP });
|
setLocalOptions({ isInPiP });
|
||||||
|
|
||||||
@ -82,7 +89,7 @@ const togglePiP = async () => {
|
|||||||
win.setWindowButtonVisibility?.(!isInPiP);
|
win.setWindowButtonVisibility?.(!isInPiP);
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockShortcutsInPiP = (event, input) => {
|
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => {
|
||||||
const key = input.key.toLowerCase();
|
const key = input.key.toLowerCase();
|
||||||
|
|
||||||
if (key === 'f') {
|
if (key === 'f') {
|
||||||
@ -93,14 +100,14 @@ const blockShortcutsInPiP = (event, input) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (_win, _options) => {
|
export default (_win: BrowserWindow, _options: PiPOptions) => {
|
||||||
options ??= _options;
|
options ??= _options;
|
||||||
win ??= _win;
|
win ??= _win;
|
||||||
setLocalOptions({ isInPiP });
|
setLocalOptions({ isInPiP });
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||||
ipcMain.on('picture-in-picture', async () => {
|
ipcMain.on('picture-in-picture', () => {
|
||||||
await togglePiP();
|
togglePiP();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.setOptions = setLocalOptions;
|
export const setOptions = setLocalOptions;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
import { ipcRenderer } from 'electron';
|
||||||
const { toKeyEvent } = require('keyboardevent-from-electron-accelerator');
|
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||||
const keyEventAreEqual = require('keyboardevents-areequal');
|
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||||
|
|
||||||
const { getSongMenu } = require('../../providers/dom-elements');
|
const { getSongMenu } = require('../../providers/dom-elements');
|
||||||
const { ElementFromFile, templatePath } = require('../utils');
|
const { ElementFromFile, templatePath } = require('../utils');
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const prompt = require('custom-electron-prompt');
|
const prompt = require('custom-electron-prompt');
|
||||||
|
|
||||||
const { setOptions } = require('./back.js');
|
const { setOptions } = require('./back.ts');
|
||||||
|
|
||||||
const promptOptions = require('../../providers/prompt-options');
|
const promptOptions = require('../../providers/prompt-options');
|
||||||
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const { ipcMain, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// Creates a DOM element from a HTML string
|
|
||||||
module.exports.ElementFromHtml = (html) => {
|
|
||||||
const template = document.createElement('template');
|
|
||||||
html = html.trim(); // Never return a text node of whitespace as the result
|
|
||||||
template.innerHTML = html;
|
|
||||||
return template.content.firstChild;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Creates a DOM element from a HTML file
|
|
||||||
module.exports.ElementFromFile = (filepath) => module.exports.ElementFromHtml(fs.readFileSync(filepath, 'utf8'));
|
|
||||||
|
|
||||||
module.exports.templatePath = (pluginPath, name) => path.join(pluginPath, 'templates', name);
|
|
||||||
|
|
||||||
module.exports.triggerAction = (channel, action, ...args) => ipcRenderer.send(channel, action, ...args);
|
|
||||||
|
|
||||||
module.exports.triggerActionSync = (channel, action, ...args) => ipcRenderer.sendSync(channel, action, ...args);
|
|
||||||
|
|
||||||
module.exports.listenAction = (channel, callback) => ipcMain.on(channel, callback);
|
|
||||||
|
|
||||||
module.exports.fileExists = (
|
|
||||||
path,
|
|
||||||
callbackIfExists,
|
|
||||||
callbackIfError = undefined,
|
|
||||||
) => {
|
|
||||||
fs.access(path, fs.F_OK, (error) => {
|
|
||||||
if (error) {
|
|
||||||
if (callbackIfError) {
|
|
||||||
callbackIfError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackIfExists();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cssToInject = new Map();
|
|
||||||
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
|
||||||
if (cssToInject.size === 0) {
|
|
||||||
setupCssInjection(webContents);
|
|
||||||
}
|
|
||||||
|
|
||||||
cssToInject.set(filepath, cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupCssInjection = (webContents) => {
|
|
||||||
webContents.on('did-finish-load', () => {
|
|
||||||
cssToInject.forEach(async (cb, filepath) => {
|
|
||||||
await webContents.insertCSS(fs.readFileSync(filepath, 'utf8'));
|
|
||||||
cb?.();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.getAllPlugins = () => {
|
|
||||||
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
|
|
||||||
return fs
|
|
||||||
.readdirSync(__dirname)
|
|
||||||
.map((name) => path.join(__dirname, name))
|
|
||||||
.filter(isDirectory)
|
|
||||||
.map((name) => path.basename(name));
|
|
||||||
};
|
|
||||||
74
plugins/utils.ts
Normal file
74
plugins/utils.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { ipcMain, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import { ValueOf } from '../utils/type-utils';
|
||||||
|
|
||||||
|
|
||||||
|
// Creates a DOM element from an HTML string
|
||||||
|
export const ElementFromHtml = (html: string) => {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
html = html.trim(); // Never return a text node of whitespace as the result
|
||||||
|
template.innerHTML = html;
|
||||||
|
return template.content.firstChild;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a DOM element from a HTML file
|
||||||
|
export const ElementFromFile = (filepath: fs.PathOrFileDescriptor) => ElementFromHtml(fs.readFileSync(filepath, 'utf8'));
|
||||||
|
|
||||||
|
export const templatePath = (pluginPath: string, name: string) => path.join(pluginPath, 'templates', name);
|
||||||
|
|
||||||
|
export const Actions = {
|
||||||
|
NEXT: 'next',
|
||||||
|
BACK: 'back',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerAction = <Parameters extends unknown[]>(channel: string, action: ValueOf<typeof Actions>, ...args: Parameters) => ipcRenderer.send(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 fileExists = (
|
||||||
|
path: fs.PathLike,
|
||||||
|
callbackIfExists: { (): void; (): void; (): void; },
|
||||||
|
callbackIfError: (() => void) | undefined = undefined,
|
||||||
|
) => {
|
||||||
|
fs.access(path, fs.constants.F_OK, (error) => {
|
||||||
|
if (error) {
|
||||||
|
callbackIfError?.();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackIfExists();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssToInject = new Map();
|
||||||
|
export const injectCSS = (webContents: Electron.WebContents, filepath: unknown, cb = undefined) => {
|
||||||
|
if (cssToInject.size === 0) {
|
||||||
|
setupCssInjection(webContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
cssToInject.set(filepath, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupCssInjection = (webContents: Electron.WebContents) => {
|
||||||
|
webContents.on('did-finish-load', () => {
|
||||||
|
cssToInject.forEach(async (callback: () => void | undefined, filepath: fs.PathOrFileDescriptor) => {
|
||||||
|
await webContents.insertCSS(fs.readFileSync(filepath, 'utf8'));
|
||||||
|
callback?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllPlugins = () => {
|
||||||
|
const isDirectory = (source: fs.PathLike) => fs.lstatSync(source).isDirectory();
|
||||||
|
return fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
|
.map((name) => path.join(__dirname, name))
|
||||||
|
.filter(isDirectory)
|
||||||
|
.map((name) => path.basename(name));
|
||||||
|
};
|
||||||
@ -1,17 +1,24 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
import { ipcRenderer } from 'electron';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
|
|
||||||
|
import config from './config';
|
||||||
|
import { fileExists } from './plugins/utils';
|
||||||
|
import setupSongInfo from './providers/song-info-front';
|
||||||
|
import { setupSongControls } from './providers/song-controls-front';
|
||||||
|
import { startingPages } from './providers/extracted-data';
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
const { fileExists } = require('./plugins/utils');
|
|
||||||
const setupSongInfo = require('./providers/song-info-front');
|
|
||||||
const { setupSongControls } = require('./providers/song-controls-front');
|
|
||||||
const { startingPages } = require('./providers/extracted-data');
|
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
const $ = document.querySelector.bind(document);
|
const $ = document.querySelector.bind(document);
|
||||||
|
|
||||||
let api;
|
let api: Element | null = null;
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
CHANNEL: string;
|
||||||
|
ACTIONS: Record<string, string>,
|
||||||
|
actions: Record<string, () => void>,
|
||||||
|
}
|
||||||
|
|
||||||
plugins.forEach(async ([plugin, options]) => {
|
plugins.forEach(async ([plugin, options]) => {
|
||||||
const preloadPath = await ipcRenderer.invoke(
|
const preloadPath = await ipcRenderer.invoke(
|
||||||
@ -20,9 +27,10 @@ plugins.forEach(async ([plugin, options]) => {
|
|||||||
'plugins',
|
'plugins',
|
||||||
plugin,
|
plugin,
|
||||||
'preload.js',
|
'preload.js',
|
||||||
);
|
) as string;
|
||||||
fileExists(preloadPath, () => {
|
fileExists(preloadPath, () => {
|
||||||
const run = require(preloadPath);
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const run = require(preloadPath) as (config: typeof options) => Promise<void>;
|
||||||
run(options);
|
run(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,14 +40,16 @@ plugins.forEach(async ([plugin, options]) => {
|
|||||||
'plugins',
|
'plugins',
|
||||||
plugin,
|
plugin,
|
||||||
'actions.js',
|
'actions.js',
|
||||||
);
|
) as string;
|
||||||
fileExists(actionPath, () => {
|
fileExists(actionPath, () => {
|
||||||
const actions = require(actionPath).actions || {};
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const actions = (require(actionPath) as Actions).actions ?? {};
|
||||||
|
|
||||||
// TODO: re-enable once contextIsolation is set to true
|
// TODO: re-enable once contextIsolation is set to true
|
||||||
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
|
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
|
||||||
for (const actionName of Object.keys(actions)) {
|
for (const actionName of Object.keys(actions)) {
|
||||||
global[actionName] = actions[actionName];
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||||
|
(global as any)[actionName] = actions[actionName];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -52,9 +62,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
'plugins',
|
'plugins',
|
||||||
plugin,
|
plugin,
|
||||||
'front.js',
|
'front.js',
|
||||||
);
|
) as string;
|
||||||
fileExists(pluginPath, () => {
|
fileExists(pluginPath, () => {
|
||||||
const run = require(pluginPath);
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const run = require(pluginPath) as (config: typeof options) => Promise<void>;
|
||||||
run(options);
|
run(options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -69,14 +80,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setupSongControls();
|
setupSongControls();
|
||||||
|
|
||||||
// Add action for reloading
|
// Add action for reloading
|
||||||
global.reload = () => ipcRenderer.send('reload');
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||||
|
(global as any).reload = () => ipcRenderer.send('reload');
|
||||||
|
|
||||||
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||||
setInterval(() => window._lact = Date.now(), 900_000);
|
setInterval(() => window._lact = Date.now(), 900_000);
|
||||||
|
|
||||||
// Setup back to front logger
|
// Setup back to front logger
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
ipcRenderer.on('log', (_event, log) => {
|
ipcRenderer.on('log', (_event, log: string) => {
|
||||||
console.log(JSON.parse(log));
|
console.log(JSON.parse(log));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -100,8 +112,12 @@ function listenForApiLoad() {
|
|||||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface YouTubeMusicAppElement extends HTMLElement {
|
||||||
|
navigate_(page: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
function onApiLoaded() {
|
function onApiLoaded() {
|
||||||
const video = $('video');
|
const video = $('video')!;
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
const audioSource = audioContext.createMediaElementSource(video);
|
const audioSource = audioContext.createMediaElementSource(video);
|
||||||
audioSource.connect(audioContext.destination);
|
audioSource.connect(audioContext.destination);
|
||||||
@ -126,29 +142,29 @@ function onApiLoaded() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);!
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||||
ipcRenderer.send('apiLoaded');
|
ipcRenderer.send('apiLoaded');
|
||||||
|
|
||||||
// Navigate to "Starting page"
|
// Navigate to "Starting page"
|
||||||
const startingPage = config.get('options.startingPage');
|
const startingPage: string = config.get('options.startingPage');
|
||||||
if (startingPage && startingPages[startingPage]) {
|
if (startingPage && startingPages[startingPage]) {
|
||||||
$('ytmusic-app')?.navigate_(startingPages[startingPage]);
|
($('ytmusic-app') as YouTubeMusicAppElement)?.navigate_(startingPages[startingPage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove upgrade button
|
// Remove upgrade button
|
||||||
if (config.get('options.removeUpgradeButton')) {
|
if (config.get('options.removeUpgradeButton')) {
|
||||||
const upgradeButton = $('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]');
|
const upgradeButton: HTMLElement | null = $('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]');
|
||||||
if (upgradeButton) {
|
if (upgradeButton) {
|
||||||
upgradeButton.style.display = 'none';
|
upgradeButton.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide / Force show like buttons
|
// Hide / Force show like buttons
|
||||||
const likeButtonsOptions = config.get('options.likeButtons');
|
const likeButtonsOptions: string = config.get('options.likeButtons');
|
||||||
if (likeButtonsOptions) {
|
if (likeButtonsOptions) {
|
||||||
const likeButtons = $('ytmusic-like-button-renderer');
|
const likeButtons: HTMLElement | null = $('ytmusic-like-button-renderer');
|
||||||
if (likeButtons) {
|
if (likeButtons) {
|
||||||
likeButtons.style.display
|
likeButtons.style.display
|
||||||
= {
|
= {
|
||||||
@ -1,35 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
module.exports.restart = () => {
|
|
||||||
process.type === 'browser' ? restart() : ipcRenderer.send('restart');
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.setupAppControls = () => {
|
|
||||||
ipcMain.on('restart', restart);
|
|
||||||
ipcMain.handle('getDownloadsFolder', () => app.getPath('downloads'));
|
|
||||||
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get('url')));
|
|
||||||
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
|
|
||||||
};
|
|
||||||
|
|
||||||
function restart() {
|
|
||||||
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
|
||||||
// ExecPath will be undefined if not running portable app, resulting in default behavior
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToFront(channel, ...args) {
|
|
||||||
for (const win of BrowserWindow.getAllWindows()) {
|
|
||||||
win.webContents.send(channel, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.sendToFront
|
|
||||||
= process.type === 'browser'
|
|
||||||
? sendToFront
|
|
||||||
: () => {
|
|
||||||
console.error('sendToFront called from renderer');
|
|
||||||
};
|
|
||||||
35
providers/app-controls.ts
Normal file
35
providers/app-controls.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { app, BrowserWindow, ipcMain, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
export const restart = () => {
|
||||||
|
process.type === 'browser' ? restartInternal() : ipcRenderer.send('restart');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setupAppControls = () => {
|
||||||
|
ipcMain.on('restart', restart);
|
||||||
|
ipcMain.handle('getDownloadsFolder', () => app.getPath('downloads'));
|
||||||
|
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow()?.webContents.loadURL(config.get('url')));
|
||||||
|
ipcMain.handle('getPath', (_, ...args: string[]) => path.join(...args));
|
||||||
|
};
|
||||||
|
|
||||||
|
function restartInternal() {
|
||||||
|
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
||||||
|
// ExecPath will be undefined if not running portable app, resulting in default behavior
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToFrontInternal(channel: string, ...args: unknown[]) {
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) {
|
||||||
|
win.webContents.send(channel, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendToFront
|
||||||
|
= process.type === 'browser'
|
||||||
|
? sendToFrontInternal
|
||||||
|
: () => {
|
||||||
|
console.error('sendToFront called from renderer');
|
||||||
|
};
|
||||||
@ -1,52 +1,28 @@
|
|||||||
module.exports = {
|
export function singleton<T extends (...params: never[]) => unknown>(fn: T): T {
|
||||||
singleton,
|
|
||||||
debounce,
|
|
||||||
cache,
|
|
||||||
throttle,
|
|
||||||
memoize,
|
|
||||||
retry,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @param {T} fn
|
|
||||||
* @returns {T}
|
|
||||||
*/
|
|
||||||
function singleton(fn) {
|
|
||||||
let called = false;
|
let called = false;
|
||||||
return (...args) => {
|
|
||||||
|
return ((...args) => {
|
||||||
if (called) {
|
if (called) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
called = true;
|
called = true;
|
||||||
return fn(...args);
|
return fn(...args);
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function debounce<T extends (...params: never[]) => unknown>(fn: T, delay: number): T {
|
||||||
* @template T
|
let timeout: NodeJS.Timeout;
|
||||||
* @param {T} fn
|
return ((...args) => {
|
||||||
* @param {number} delay
|
|
||||||
* @returns {T}
|
|
||||||
*/
|
|
||||||
function debounce(fn, delay) {
|
|
||||||
let timeout;
|
|
||||||
return (...args) => {
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(() => fn(...args), delay);
|
timeout = setTimeout(() => fn(...args), delay);
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function cache<T extends (...params: P) => R, P extends never[], R>(fn: T): T {
|
||||||
* @template T
|
let lastArgs: P;
|
||||||
* @param {T} fn
|
let lastResult: R;
|
||||||
* @returns {T}
|
return ((...args: P) => {
|
||||||
*/
|
|
||||||
function cache(fn) {
|
|
||||||
let lastArgs;
|
|
||||||
let lastResult;
|
|
||||||
return (...args) => {
|
|
||||||
if (
|
if (
|
||||||
args.length !== lastArgs?.length
|
args.length !== lastArgs?.length
|
||||||
|| args.some((arg, i) => arg !== lastArgs[i])
|
|| args.some((arg, i) => arg !== lastArgs[i])
|
||||||
@ -56,22 +32,16 @@ function cache(fn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return lastResult;
|
return lastResult;
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The following are currently unused, but potentially useful in the future
|
The following are currently unused, but potentially useful in the future
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
export function throttle<T extends (...params: unknown[]) => unknown>(fn: T, delay: number): T {
|
||||||
* @template T
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
* @param {T} fn
|
return ((...args) => {
|
||||||
* @param {number} delay
|
|
||||||
* @returns {T}
|
|
||||||
*/
|
|
||||||
function throttle(fn, delay) {
|
|
||||||
let timeout;
|
|
||||||
return (...args) => {
|
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -80,33 +50,24 @@ function throttle(fn, delay) {
|
|||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
fn(...args);
|
fn(...args);
|
||||||
}, delay);
|
}, delay);
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
||||||
* @template T
|
|
||||||
* @param {T} fn
|
|
||||||
* @returns {T}
|
|
||||||
*/
|
|
||||||
function memoize(fn) {
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
return (...args) => {
|
|
||||||
|
return ((...args) => {
|
||||||
const key = JSON.stringify(args);
|
const key = JSON.stringify(args);
|
||||||
if (!cache.has(key)) {
|
if (!cache.has(key)) {
|
||||||
cache.set(key, fn(...args));
|
cache.set(key, fn(...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache.get(key);
|
return cache.get(key) as unknown;
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function retry<T extends (...params: unknown[]) => unknown>(fn: T, { retries = 3, delay = 1000 } = {}): T {
|
||||||
* @template T
|
return ((...args) => {
|
||||||
* @param {T} fn
|
|
||||||
* @returns {T}
|
|
||||||
*/
|
|
||||||
function retry(fn, { retries = 3, delay = 1000 } = {}) {
|
|
||||||
return (...args) => {
|
|
||||||
try {
|
try {
|
||||||
return fn(...args);
|
return fn(...args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,5 +78,14 @@ function retry(fn, { retries = 3, delay = 1000 } = {}) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
singleton,
|
||||||
|
debounce,
|
||||||
|
cache,
|
||||||
|
throttle,
|
||||||
|
memoize,
|
||||||
|
retry,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
const getSongMenu = () =>
|
export const getSongMenu = () =>
|
||||||
document.querySelector('ytmusic-menu-popup-renderer tp-yt-paper-listbox');
|
document.querySelector('ytmusic-menu-popup-renderer tp-yt-paper-listbox');
|
||||||
|
|
||||||
module.exports = { getSongMenu };
|
export default { getSongMenu };
|
||||||
@ -1,4 +1,4 @@
|
|||||||
const startingPages = {
|
export const startingPages: Record<string, string> = {
|
||||||
'Default': '',
|
'Default': '',
|
||||||
'Home': 'FEmusic_home',
|
'Home': 'FEmusic_home',
|
||||||
'Explore': 'FEmusic_explore',
|
'Explore': 'FEmusic_explore',
|
||||||
@ -18,6 +18,6 @@ const startingPages = {
|
|||||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
startingPages,
|
startingPages,
|
||||||
};
|
};
|
||||||
@ -1,13 +1,13 @@
|
|||||||
const { Titlebar, Color } = require('custom-electron-titlebar');
|
import { Titlebar, Color } from 'custom-electron-titlebar';
|
||||||
|
|
||||||
module.exports = () => {
|
export default () => {
|
||||||
new Titlebar({
|
new Titlebar({
|
||||||
backgroundColor: Color.fromHex('#050505'),
|
backgroundColor: Color.fromHex('#050505'),
|
||||||
minimizable: false,
|
minimizable: false,
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
menu: null,
|
menu: undefined,
|
||||||
});
|
});
|
||||||
const mainStyle = document.querySelector('#container').style;
|
const mainStyle = (document.querySelector('#container') as HTMLElement)!.style;
|
||||||
mainStyle.width = '100%';
|
mainStyle.width = '100%';
|
||||||
mainStyle.position = 'fixed';
|
mainStyle.position = 'fixed';
|
||||||
mainStyle.border = 'unset';
|
mainStyle.border = 'unset';
|
||||||
@ -1,8 +1,8 @@
|
|||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
|
|
||||||
const { isEnabled } = require('../config/plugins');
|
import { isEnabled } from '../config/plugins';
|
||||||
|
|
||||||
const iconPath = path.join(__dirname, '..', 'assets', 'youtube-music-tray.png');
|
const iconPath = path.join(__dirname, '..', 'assets', 'youtube-music-tray.png');
|
||||||
const customTitlebarPath = path.join(__dirname, 'prompt-custom-titlebar.js');
|
const customTitlebarPath = path.join(__dirname, 'prompt-custom-titlebar.js');
|
||||||
@ -17,4 +17,4 @@ const promptOptions = !is.macOS() && isEnabled('in-app-menu') ? {
|
|||||||
icon: iconPath,
|
icon: iconPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => promptOptions;
|
export default () => promptOptions;
|
||||||
@ -1,45 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const { app } = require('electron');
|
|
||||||
|
|
||||||
const getSongControls = require('./song-controls');
|
|
||||||
|
|
||||||
const APP_PROTOCOL = 'youtubemusic';
|
|
||||||
|
|
||||||
let protocolHandler;
|
|
||||||
|
|
||||||
function setupProtocolHandler(win) {
|
|
||||||
if (process.defaultApp && process.argv.length >= 2) {
|
|
||||||
app.setAsDefaultProtocolClient(
|
|
||||||
APP_PROTOCOL,
|
|
||||||
process.execPath,
|
|
||||||
[path.resolve(process.argv[1])],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
app.setAsDefaultProtocolClient(APP_PROTOCOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
const songControls = getSongControls(win);
|
|
||||||
|
|
||||||
protocolHandler = (cmd) => {
|
|
||||||
if (Object.keys(songControls).includes(cmd)) {
|
|
||||||
songControls[cmd]();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleProtocol(cmd) {
|
|
||||||
protocolHandler(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeProtocolHandler(f) {
|
|
||||||
protocolHandler = f;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
APP_PROTOCOL,
|
|
||||||
setupProtocolHandler,
|
|
||||||
handleProtocol,
|
|
||||||
changeProtocolHandler,
|
|
||||||
};
|
|
||||||
|
|
||||||
45
providers/protocol-handler.ts
Normal file
45
providers/protocol-handler.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
import getSongControls from './song-controls';
|
||||||
|
|
||||||
|
export const APP_PROTOCOL = 'youtubemusic';
|
||||||
|
|
||||||
|
let protocolHandler: ((cmd: string) => void) | undefined;
|
||||||
|
|
||||||
|
export function setupProtocolHandler(win: BrowserWindow) {
|
||||||
|
if (process.defaultApp && process.argv.length >= 2) {
|
||||||
|
app.setAsDefaultProtocolClient(
|
||||||
|
APP_PROTOCOL,
|
||||||
|
process.execPath,
|
||||||
|
[path.resolve(process.argv[1])],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
app.setAsDefaultProtocolClient(APP_PROTOCOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const songControls = getSongControls(win);
|
||||||
|
|
||||||
|
protocolHandler = ((cmd: keyof typeof songControls) => {
|
||||||
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
|
songControls[cmd]();
|
||||||
|
}
|
||||||
|
}) as (cmd: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleProtocol(cmd: string) {
|
||||||
|
protocolHandler?.(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeProtocolHandler(f: (cmd: string) => void) {
|
||||||
|
protocolHandler = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
APP_PROTOCOL,
|
||||||
|
setupProtocolHandler,
|
||||||
|
handleProtocol,
|
||||||
|
changeProtocolHandler,
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
module.exports.setupSongControls = () => {
|
|
||||||
document.addEventListener('apiLoaded', (e) => {
|
|
||||||
ipcRenderer.on('seekTo', (_, t) => e.detail.seekTo(t));
|
|
||||||
ipcRenderer.on('seekBy', (_, t) => e.detail.seekBy(t));
|
|
||||||
}, { once: true, passive: true });
|
|
||||||
};
|
|
||||||
8
providers/song-controls-front.ts
Normal file
8
providers/song-controls-front.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
export const setupSongControls = () => {
|
||||||
|
document.addEventListener('apiLoaded', (event) => {
|
||||||
|
ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t));
|
||||||
|
ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t));
|
||||||
|
}, { once: true, passive: true });
|
||||||
|
};
|
||||||
@ -1,13 +1,16 @@
|
|||||||
// This is used for to control the songs
|
// This is used for to control the songs
|
||||||
const pressKey = (window, key, modifiers = []) => {
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
type Modifiers = (Electron.MouseInputEvent | Electron.MouseWheelInputEvent | Electron.KeyboardInputEvent)['modifiers'];
|
||||||
|
export const pressKey = (window: BrowserWindow, key: string, modifiers: Modifiers = []) => {
|
||||||
window.webContents.sendInputEvent({
|
window.webContents.sendInputEvent({
|
||||||
type: 'keydown',
|
type: 'keyDown',
|
||||||
modifiers,
|
modifiers,
|
||||||
keyCode: key,
|
keyCode: key,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (win) => {
|
export default (win: BrowserWindow) => {
|
||||||
const commands = {
|
const commands = {
|
||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, 'k'),
|
previous: () => pressKey(win, 'k'),
|
||||||
@ -1,120 +0,0 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
const { getImage } = require('./song-info');
|
|
||||||
|
|
||||||
const { singleton } = require('../providers/decorators');
|
|
||||||
|
|
||||||
global.songInfo = {};
|
|
||||||
|
|
||||||
const $ = (s) => document.querySelector(s);
|
|
||||||
const $$ = (s) => [...document.querySelectorAll(s)];
|
|
||||||
|
|
||||||
ipcRenderer.on('update-song-info', async (_, extractedSongInfo) => {
|
|
||||||
global.songInfo = JSON.parse(extractedSongInfo);
|
|
||||||
global.songInfo.image = await getImage(global.songInfo.imageSrc);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
|
||||||
const srcChangedEvent = new CustomEvent('srcChanged');
|
|
||||||
|
|
||||||
const setupSeekedListener = singleton(() => {
|
|
||||||
$('video')?.addEventListener('seeked', (v) => ipcRenderer.send('seeked', v.target.currentTime));
|
|
||||||
});
|
|
||||||
module.exports.setupSeekedListener = setupSeekedListener;
|
|
||||||
|
|
||||||
const setupTimeChangedListener = singleton(() => {
|
|
||||||
const progressObserver = new MutationObserver((mutations) => {
|
|
||||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
|
||||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
|
||||||
});
|
|
||||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ['value'] });
|
|
||||||
});
|
|
||||||
module.exports.setupTimeChangedListener = setupTimeChangedListener;
|
|
||||||
|
|
||||||
const setupRepeatChangedListener = singleton(() => {
|
|
||||||
const repeatObserver = new MutationObserver((mutations) => {
|
|
||||||
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
|
|
||||||
});
|
|
||||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ['title'] });
|
|
||||||
|
|
||||||
// Emit the initial value as well; as it's persistent between launches.
|
|
||||||
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
|
|
||||||
});
|
|
||||||
module.exports.setupRepeatChangedListener = setupRepeatChangedListener;
|
|
||||||
|
|
||||||
const setupVolumeChangedListener = singleton((api) => {
|
|
||||||
$('video').addEventListener('volumechange', () => {
|
|
||||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
|
||||||
});
|
|
||||||
// Emit the initial value as well; as it's persistent between launches.
|
|
||||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
|
||||||
});
|
|
||||||
module.exports.setupVolumeChangedListener = setupVolumeChangedListener;
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
|
||||||
ipcRenderer.on('setupTimeChangedListener', async () => {
|
|
||||||
setupTimeChangedListener();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on('setupRepeatChangedListener', async () => {
|
|
||||||
setupRepeatChangedListener();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on('setupVolumeChangedListener', async () => {
|
|
||||||
setupVolumeChangedListener(apiEvent.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on('setupSeekedListener', async () => {
|
|
||||||
setupSeekedListener();
|
|
||||||
});
|
|
||||||
|
|
||||||
const playPausedHandler = (e, status) => {
|
|
||||||
if (Math.round(e.target.currentTime) > 0) {
|
|
||||||
ipcRenderer.send('playPaused', {
|
|
||||||
isPaused: status === 'pause',
|
|
||||||
elapsedSeconds: Math.floor(e.target.currentTime),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const playPausedHandlers = {
|
|
||||||
playing: (e) => playPausedHandler(e, 'playing'),
|
|
||||||
pause: (e) => playPausedHandler(e, 'pause'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Name = "dataloaded" and abit later "dataupdated"
|
|
||||||
apiEvent.detail.addEventListener('videodatachange', (name) => {
|
|
||||||
if (name !== 'dataloaded') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const video = $('video');
|
|
||||||
|
|
||||||
video.dispatchEvent(srcChangedEvent);
|
|
||||||
for (const status of ['playing', 'pause']) { // for fix issue that pause event not fired
|
|
||||||
video.addEventListener(status, playPausedHandlers[status]);
|
|
||||||
}
|
|
||||||
setTimeout(sendSongInfo, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
const video = $('video');
|
|
||||||
for (const status of ['playing', 'pause']) {
|
|
||||||
video.addEventListener(status, playPausedHandlers[status]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendSongInfo() {
|
|
||||||
const data = apiEvent.detail.getPlayerResponse();
|
|
||||||
|
|
||||||
data.videoDetails.album = $$(
|
|
||||||
'.byline.ytmusic-player-bar > .yt-simple-endpoint',
|
|
||||||
).find((e) =>
|
|
||||||
e.href?.includes('browse/FEmusic_library_privately_owned_release')
|
|
||||||
|| e.href?.includes('browse/MPREb'),
|
|
||||||
)?.textContent;
|
|
||||||
|
|
||||||
data.videoDetails.elapsedSeconds = 0;
|
|
||||||
data.videoDetails.isPaused = false;
|
|
||||||
ipcRenderer.send('video-src-changed', JSON.stringify(data));
|
|
||||||
}
|
|
||||||
}, { once: true, passive: true });
|
|
||||||
};
|
|
||||||
124
providers/song-info-front.ts
Normal file
124
providers/song-info-front.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import { singleton } from './decorators';
|
||||||
|
import { getImage, SongInfo } from './song-info';
|
||||||
|
|
||||||
|
import { YoutubePlayer } from '../types/youtube-player';
|
||||||
|
import { GetState } from '../types/datahost-get-state';
|
||||||
|
|
||||||
|
let songInfo: SongInfo = {} as SongInfo;
|
||||||
|
|
||||||
|
const $ = <E extends HTMLElement>(s: string): E => document.querySelector(s) as E;
|
||||||
|
const $$ = <E extends HTMLElement>(s: string): E[] => [...document.querySelectorAll(s)!] as E[];
|
||||||
|
|
||||||
|
ipcRenderer.on('update-song-info', async (_, extractedSongInfo: string) => {
|
||||||
|
songInfo = JSON.parse(extractedSongInfo) as SongInfo;
|
||||||
|
if (songInfo.imageSrc) songInfo.image = await getImage(songInfo.imageSrc);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||||
|
const srcChangedEvent = new CustomEvent('srcChanged');
|
||||||
|
|
||||||
|
export const setupSeekedListener = singleton(() => {
|
||||||
|
$('video')?.addEventListener('seeked', (v) => ipcRenderer.send('seeked', (v.target as HTMLVideoElement).currentTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupTimeChangedListener = singleton(() => {
|
||||||
|
const progressObserver = new MutationObserver((mutations) => {
|
||||||
|
const target = mutations[0].target as HTMLInputElement;
|
||||||
|
ipcRenderer.send('timeChanged', target.value);
|
||||||
|
songInfo.elapsedSeconds = Number(target.value);
|
||||||
|
});
|
||||||
|
progressObserver.observe($('#progress-bar'), { attributeFilter: ['value'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupRepeatChangedListener = singleton(() => {
|
||||||
|
const repeatObserver = new MutationObserver((mutations) => {
|
||||||
|
|
||||||
|
// provided by YouTube music
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||||
|
ipcRenderer.send('repeatChanged', ((mutations[0].target as any).__dataHost.getState() as GetState).queue.repeatMode);
|
||||||
|
});
|
||||||
|
repeatObserver.observe($('#right-controls .repeat')!, { attributeFilter: ['title'] });
|
||||||
|
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
// provided by YouTube music
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
ipcRenderer.send('repeatChanged', (($('ytmusic-player-bar') as any).getState() as GetState).queue.repeatMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
|
||||||
|
$('video').addEventListener('volumechange', () => {
|
||||||
|
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||||
|
});
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||||
|
});
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||||
|
ipcRenderer.on('setupTimeChangedListener', () => {
|
||||||
|
setupTimeChangedListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('setupRepeatChangedListener', () => {
|
||||||
|
setupRepeatChangedListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('setupVolumeChangedListener', () => {
|
||||||
|
setupVolumeChangedListener(apiEvent.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('setupSeekedListener', () => {
|
||||||
|
setupSeekedListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
const playPausedHandler = (e: Event, status: string) => {
|
||||||
|
if (Math.round((e.target as HTMLVideoElement).currentTime) > 0) {
|
||||||
|
ipcRenderer.send('playPaused', {
|
||||||
|
isPaused: status === 'pause',
|
||||||
|
elapsedSeconds: Math.floor((e.target as HTMLVideoElement).currentTime),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playPausedHandlers = {
|
||||||
|
playing: (e: Event) => playPausedHandler(e, 'playing'),
|
||||||
|
pause: (e: Event) => playPausedHandler(e, 'pause'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Name = "dataloaded" and abit later "dataupdated"
|
||||||
|
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
||||||
|
if (name !== 'dataloaded') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const video = $<HTMLVideoElement>('video');
|
||||||
|
video.dispatchEvent(srcChangedEvent);
|
||||||
|
|
||||||
|
for (const status of ['playing', 'pause'] as const) { // for fix issue that pause event not fired
|
||||||
|
video.addEventListener(status, playPausedHandlers[status]);
|
||||||
|
}
|
||||||
|
setTimeout(sendSongInfo, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
const video = $('video')!;
|
||||||
|
for (const status of ['playing', 'pause'] as const) {
|
||||||
|
video.addEventListener(status, playPausedHandlers[status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSongInfo() {
|
||||||
|
const data = apiEvent.detail.getPlayerResponse();
|
||||||
|
|
||||||
|
data.videoDetails.album = $$<HTMLAnchorElement>(
|
||||||
|
'.byline.ytmusic-player-bar > .yt-simple-endpoint',
|
||||||
|
).find((e) =>
|
||||||
|
e.href?.includes('browse/FEmusic_library_privately_owned_release')
|
||||||
|
|| e.href?.includes('browse/MPREb'),
|
||||||
|
)?.textContent;
|
||||||
|
|
||||||
|
data.videoDetails.elapsedSeconds = 0;
|
||||||
|
data.videoDetails.isPaused = false;
|
||||||
|
ipcRenderer.send('video-src-changed', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}, { once: true, passive: true });
|
||||||
|
};
|
||||||
@ -1,13 +1,28 @@
|
|||||||
const { ipcMain, nativeImage, net } = require('electron');
|
import { BrowserWindow, ipcMain, nativeImage, net } from 'electron';
|
||||||
|
|
||||||
const config = require('../config');
|
import { cache } from './decorators';
|
||||||
const { cache } = require('../providers/decorators');
|
|
||||||
|
import config from '../config';
|
||||||
|
import { GetPlayerResponse } from '../types/get-player-response';
|
||||||
|
|
||||||
|
export interface SongInfo {
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
views: number;
|
||||||
|
uploadDate: string;
|
||||||
|
imageSrc?: string | null;
|
||||||
|
image?: Electron.NativeImage | null;
|
||||||
|
isPaused?: boolean;
|
||||||
|
songDuration: number;
|
||||||
|
elapsedSeconds: number;
|
||||||
|
url: string;
|
||||||
|
album?: string | null;
|
||||||
|
videoId: string;
|
||||||
|
playlistId: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Fill songInfo with empty values
|
// Fill songInfo with empty values
|
||||||
/**
|
export const songInfo: SongInfo = {
|
||||||
* @typedef {songInfo} SongInfo
|
|
||||||
*/
|
|
||||||
const songInfo = {
|
|
||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
views: 0,
|
views: 0,
|
||||||
@ -24,11 +39,9 @@ const songInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Grab the native image using the src
|
// Grab the native image using the src
|
||||||
const getImage = cache(
|
export const getImage = cache(
|
||||||
/**
|
async (src: string): Promise<Electron.NativeImage> => {
|
||||||
* @returns {Promise<Electron.NativeImage>}
|
|
||||||
*/
|
|
||||||
async (src) => {
|
|
||||||
const result = await net.fetch(src);
|
const result = await net.fetch(src);
|
||||||
const buffer = await result.arrayBuffer();
|
const buffer = await result.arrayBuffer();
|
||||||
const output = nativeImage.createFromBuffer(Buffer.from(buffer));
|
const output = nativeImage.createFromBuffer(Buffer.from(buffer));
|
||||||
@ -40,8 +53,8 @@ const getImage = cache(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleData = async (responseText, win) => {
|
const handleData = async (responseText: string, win: Electron.BrowserWindow) => {
|
||||||
const data = JSON.parse(responseText);
|
const data = JSON.parse(responseText) as GetPlayerResponse;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -50,7 +63,7 @@ const handleData = async (responseText, win) => {
|
|||||||
if (microformat) {
|
if (microformat) {
|
||||||
songInfo.uploadDate = microformat.uploadDate;
|
songInfo.uploadDate = microformat.uploadDate;
|
||||||
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
||||||
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get('list');
|
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get('list') ?? '';
|
||||||
// Used for options.resumeOnStart
|
// Used for options.resumeOnStart
|
||||||
config.set('url', microformat.urlCanonical);
|
config.set('url', microformat.urlCanonical);
|
||||||
}
|
}
|
||||||
@ -59,8 +72,8 @@ const handleData = async (responseText, win) => {
|
|||||||
if (videoDetails) {
|
if (videoDetails) {
|
||||||
songInfo.title = cleanupName(videoDetails.title);
|
songInfo.title = cleanupName(videoDetails.title);
|
||||||
songInfo.artist = cleanupName(videoDetails.author);
|
songInfo.artist = cleanupName(videoDetails.author);
|
||||||
songInfo.views = videoDetails.viewCount;
|
songInfo.views = Number(videoDetails.viewCount);
|
||||||
songInfo.songDuration = videoDetails.lengthSeconds;
|
songInfo.songDuration = Number(videoDetails.lengthSeconds);
|
||||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||||
songInfo.isPaused = videoDetails.isPaused;
|
songInfo.isPaused = videoDetails.isPaused;
|
||||||
songInfo.videoId = videoDetails.videoId;
|
songInfo.videoId = videoDetails.videoId;
|
||||||
@ -68,33 +81,26 @@ const handleData = async (responseText, win) => {
|
|||||||
|
|
||||||
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||||
songInfo.imageSrc = thumbnails.at(-1)?.url.split('?')[0];
|
songInfo.imageSrc = thumbnails.at(-1)?.url.split('?')[0];
|
||||||
songInfo.image = await getImage(songInfo.imageSrc);
|
if (songInfo.imageSrc) songInfo.image = await getImage(songInfo.imageSrc);
|
||||||
|
|
||||||
win.webContents.send('update-song-info', JSON.stringify(songInfo));
|
win.webContents.send('update-song-info', JSON.stringify(songInfo));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// This variable will be filled with the callbacks once they register
|
// This variable will be filled with the callbacks once they register
|
||||||
const callbacks = [];
|
type SongInfoCallback = (songInfo: SongInfo, event: string) => void;
|
||||||
|
const callbacks: SongInfoCallback[] = [];
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
/**
|
const registerCallback = (callback: SongInfoCallback) => {
|
||||||
* @callback songInfoCallback
|
|
||||||
* @param {songInfo} songInfo
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @param {songInfoCallback} callback
|
|
||||||
*/
|
|
||||||
const registerCallback = (callback) => {
|
|
||||||
callbacks.push(callback);
|
callbacks.push(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
let handlingData = false;
|
let handlingData = false;
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win: BrowserWindow) => {
|
||||||
// This will be called when the song-info-front finds a new request with song data
|
// This will be called when the song-info-front finds a new request with song data
|
||||||
ipcMain.on('video-src-changed', async (_, responseText) => {
|
ipcMain.on('video-src-changed', async (_, responseText: string) => {
|
||||||
handlingData = true;
|
handlingData = true;
|
||||||
await handleData(responseText, win);
|
await handleData(responseText, win);
|
||||||
handlingData = false;
|
handlingData = false;
|
||||||
@ -102,7 +108,7 @@ const registerProvider = (win) => {
|
|||||||
c(songInfo, 'video-src-changed');
|
c(songInfo, 'video-src-changed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }) => {
|
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }: { isPaused: boolean, elapsedSeconds: number }) => {
|
||||||
songInfo.isPaused = isPaused;
|
songInfo.isPaused = isPaused;
|
||||||
songInfo.elapsedSeconds = elapsedSeconds;
|
songInfo.elapsedSeconds = elapsedSeconds;
|
||||||
if (handlingData) {
|
if (handlingData) {
|
||||||
@ -122,7 +128,7 @@ const suffixesToRemove = [
|
|||||||
' (clip officiel)',
|
' (clip officiel)',
|
||||||
];
|
];
|
||||||
|
|
||||||
function cleanupName(name) {
|
export function cleanupName(name: string): string {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@ -138,7 +144,5 @@ function cleanupName(name) {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerCallback;
|
export default registerCallback;
|
||||||
module.exports.setupSongInfo = registerProvider;
|
export const setupSongInfo = registerProvider;
|
||||||
module.exports.getImage = getImage;
|
|
||||||
module.exports.cleanupName = cleanupName;
|
|
||||||
15
reset.d.ts
vendored
Normal file
15
reset.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import '@total-typescript/ts-reset';
|
||||||
|
import { YoutubePlayer } from './types/youtube-player';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface DocumentEventMap {
|
||||||
|
'apiLoaded': CustomEvent<YoutubePlayer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
/**
|
||||||
|
* YouTube Music internal variable (Last interaction time)
|
||||||
|
*/
|
||||||
|
_lact: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
const path = require('node:path');
|
import path from 'node:path';
|
||||||
|
|
||||||
const { Menu, nativeImage, Tray } = require('electron');
|
import { Menu, nativeImage, Tray } from 'electron';
|
||||||
|
|
||||||
const { restart } = require('./providers/app-controls');
|
import { restart } from './providers/app-controls';
|
||||||
const config = require('./config');
|
import config from './config';
|
||||||
const getSongControls = require('./providers/song-controls');
|
import getSongControls from './providers/song-controls';
|
||||||
|
|
||||||
|
import type { MenuTemplate } from './menu';
|
||||||
|
|
||||||
// Prevent tray being garbage collected
|
// Prevent tray being garbage collected
|
||||||
|
let tray: Electron.Tray | undefined;
|
||||||
|
|
||||||
/** @type {Electron.Tray} */
|
type TrayEvent = (event: Electron.KeyboardEvent, bounds: Electron.Rectangle) => void;
|
||||||
let tray;
|
|
||||||
|
|
||||||
module.exports.setTrayOnClick = (fn) => {
|
export const setTrayOnClick = (fn: TrayEvent) => {
|
||||||
if (!tray) {
|
if (!tray) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -20,8 +22,8 @@ module.exports.setTrayOnClick = (fn) => {
|
|||||||
tray.on('click', fn);
|
tray.on('click', fn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wont do anything on macos since its disabled
|
// Won't do anything on macOS since its disabled
|
||||||
module.exports.setTrayOnDoubleClick = (fn) => {
|
export const setTrayOnDoubleClick = (fn: TrayEvent) => {
|
||||||
if (!tray) {
|
if (!tray) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -30,7 +32,7 @@ module.exports.setTrayOnDoubleClick = (fn) => {
|
|||||||
tray.on('double-click', fn);
|
tray.on('double-click', fn);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.setUpTray = (app, win) => {
|
export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
|
||||||
if (!config.get('options.tray')) {
|
if (!config.get('options.tray')) {
|
||||||
tray = undefined;
|
tray = undefined;
|
||||||
return;
|
return;
|
||||||
@ -63,7 +65,7 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const template = [
|
const template: MenuTemplate = [
|
||||||
{
|
{
|
||||||
label: 'Play/Pause',
|
label: 'Play/Pause',
|
||||||
click() {
|
click() {
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"*.config.ts",
|
||||||
|
"./dist"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"*": ["*.d.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
1823
types/datahost-get-state.ts
Normal file
1823
types/datahost-get-state.ts
Normal file
File diff suppressed because it is too large
Load Diff
464
types/get-player-response.ts
Normal file
464
types/get-player-response.ts
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
export interface GetPlayerResponse {
|
||||||
|
responseContext: ResponseContext;
|
||||||
|
playabilityStatus: PlayabilityStatus;
|
||||||
|
streamingData: StreamingData;
|
||||||
|
heartbeatParams: HeartbeatParams;
|
||||||
|
playbackTracking: PlaybackTracking;
|
||||||
|
captions: Captions;
|
||||||
|
videoDetails: GetPlayerResponseVideoDetails;
|
||||||
|
playerConfig: PlayerConfig;
|
||||||
|
storyboards: Storyboards;
|
||||||
|
microformat: Microformat;
|
||||||
|
trackingParams: string;
|
||||||
|
attestation: Attestation;
|
||||||
|
endscreen: Endscreen;
|
||||||
|
adBreakHeartbeatParams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attestation {
|
||||||
|
playerAttestationRenderer: PlayerAttestationRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAttestationRenderer {
|
||||||
|
challenge: string;
|
||||||
|
botguardData: BotguardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotguardData {
|
||||||
|
program: string;
|
||||||
|
interpreterSafeUrl: InterpreterSafeURL;
|
||||||
|
serverEnvironment: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterpreterSafeURL {
|
||||||
|
privateDoNotAccessOrElseTrustedResourceUrlWrappedValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Captions {
|
||||||
|
playerCaptionsTracklistRenderer: PlayerCaptionsTracklistRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerCaptionsTracklistRenderer {
|
||||||
|
captionTracks: CaptionTrack[];
|
||||||
|
audioTracks: AudioTrack[];
|
||||||
|
translationLanguages: TranslationLanguage[];
|
||||||
|
defaultAudioTrackIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioTrack {
|
||||||
|
captionTrackIndices: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptionTrack {
|
||||||
|
baseUrl: string;
|
||||||
|
name: Name;
|
||||||
|
vssId: string;
|
||||||
|
languageCode: string;
|
||||||
|
kind: string;
|
||||||
|
isTranslatable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Name {
|
||||||
|
runs: Run[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Run {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranslationLanguage {
|
||||||
|
languageCode: string;
|
||||||
|
languageName: Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Endscreen {
|
||||||
|
endscreenRenderer: EndscreenRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EndscreenRenderer {
|
||||||
|
elements: Element[];
|
||||||
|
startMs: string;
|
||||||
|
trackingParams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Element {
|
||||||
|
endscreenElementRenderer: EndscreenElementRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EndscreenElementRenderer {
|
||||||
|
style: string;
|
||||||
|
image: ImageClass;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
top: number;
|
||||||
|
aspectRatio: number;
|
||||||
|
startMs: string;
|
||||||
|
endMs: string;
|
||||||
|
title: Title;
|
||||||
|
metadata: Name;
|
||||||
|
endpoint: Endpoint;
|
||||||
|
trackingParams: string;
|
||||||
|
id: string;
|
||||||
|
thumbnailOverlays: ThumbnailOverlay[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Endpoint {
|
||||||
|
clickTrackingParams: string;
|
||||||
|
commandMetadata: CommandMetadata;
|
||||||
|
watchEndpoint: WatchEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandMetadata {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchEndpoint {
|
||||||
|
videoId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageClass {
|
||||||
|
thumbnails: ThumbnailElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailElement {
|
||||||
|
url: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailOverlay {
|
||||||
|
thumbnailOverlayTimeStatusRenderer: ThumbnailOverlayTimeStatusRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailOverlayTimeStatusRenderer {
|
||||||
|
text: Title;
|
||||||
|
style: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Title {
|
||||||
|
runs: Run[];
|
||||||
|
accessibility: Accessibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Accessibility {
|
||||||
|
accessibilityData: AccessibilityData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessibilityData {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeartbeatParams {
|
||||||
|
heartbeatToken: string;
|
||||||
|
intervalMilliseconds: string;
|
||||||
|
maxRetries: string;
|
||||||
|
drmSessionId: string;
|
||||||
|
softFailOnError: boolean;
|
||||||
|
heartbeatServerData: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Microformat {
|
||||||
|
microformatDataRenderer: MicroformatDataRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MicroformatDataRenderer {
|
||||||
|
urlCanonical: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
thumbnail: ImageClass;
|
||||||
|
siteName: string;
|
||||||
|
appName: string;
|
||||||
|
androidPackage: string;
|
||||||
|
iosAppStoreId: string;
|
||||||
|
iosAppArguments: string;
|
||||||
|
ogType: string;
|
||||||
|
urlApplinksIos: string;
|
||||||
|
urlApplinksAndroid: string;
|
||||||
|
urlTwitterIos: string;
|
||||||
|
urlTwitterAndroid: string;
|
||||||
|
twitterCardType: string;
|
||||||
|
twitterSiteHandle: string;
|
||||||
|
schemaDotOrgType: string;
|
||||||
|
noindex: boolean;
|
||||||
|
unlisted: boolean;
|
||||||
|
paid: boolean;
|
||||||
|
familySafe: boolean;
|
||||||
|
tags: string[];
|
||||||
|
availableCountries: string[];
|
||||||
|
pageOwnerDetails: PageOwnerDetails;
|
||||||
|
videoDetails: MicroformatDataRendererVideoDetails;
|
||||||
|
linkAlternates: LinkAlternate[];
|
||||||
|
viewCount: string;
|
||||||
|
publishDate: string;
|
||||||
|
category: string;
|
||||||
|
uploadDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkAlternate {
|
||||||
|
hrefUrl: string;
|
||||||
|
title?: string;
|
||||||
|
alternateType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageOwnerDetails {
|
||||||
|
name: string;
|
||||||
|
externalChannelId: string;
|
||||||
|
youtubeProfileUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MicroformatDataRendererVideoDetails {
|
||||||
|
externalVideoId: string;
|
||||||
|
durationSeconds: string;
|
||||||
|
durationIso8601: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayabilityStatus {
|
||||||
|
status: string;
|
||||||
|
playableInEmbed: boolean;
|
||||||
|
audioOnlyPlayability: AudioOnlyPlayability;
|
||||||
|
miniplayer: Miniplayer;
|
||||||
|
contextParams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioOnlyPlayability {
|
||||||
|
audioOnlyPlayabilityRenderer: AudioOnlyPlayabilityRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioOnlyPlayabilityRenderer {
|
||||||
|
trackingParams: string;
|
||||||
|
audioOnlyAvailability: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Miniplayer {
|
||||||
|
miniplayerRenderer: MiniplayerRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MiniplayerRenderer {
|
||||||
|
playbackMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaybackTracking {
|
||||||
|
videostatsPlaybackUrl: PtrackingURLClass;
|
||||||
|
videostatsDelayplayUrl: AtrURLClass;
|
||||||
|
videostatsWatchtimeUrl: PtrackingURLClass;
|
||||||
|
ptrackingUrl: PtrackingURLClass;
|
||||||
|
qoeUrl: PtrackingURLClass;
|
||||||
|
atrUrl: AtrURLClass;
|
||||||
|
videostatsScheduledFlushWalltimeSeconds: number[];
|
||||||
|
videostatsDefaultFlushIntervalSeconds: number;
|
||||||
|
googleRemarketingUrl: AtrURLClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AtrURLClass {
|
||||||
|
baseUrl: string;
|
||||||
|
elapsedMediaTimeSeconds: number;
|
||||||
|
headers: Header[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Header {
|
||||||
|
headerType: HeaderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HeaderType {
|
||||||
|
PlusPageID = 'PLUS_PAGE_ID',
|
||||||
|
UserAuth = 'USER_AUTH',
|
||||||
|
VisitorID = 'VISITOR_ID',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PtrackingURLClass {
|
||||||
|
baseUrl: string;
|
||||||
|
headers: Header[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerConfig {
|
||||||
|
audioConfig: AudioConfig;
|
||||||
|
streamSelectionConfig: StreamSelectionConfig;
|
||||||
|
mediaCommonConfig: MediaCommonConfig;
|
||||||
|
webPlayerConfig: WebPlayerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioConfig {
|
||||||
|
loudnessDb: number;
|
||||||
|
perceptualLoudnessDb: number;
|
||||||
|
enablePerFormatLoudness: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaCommonConfig {
|
||||||
|
dynamicReadaheadConfig: DynamicReadaheadConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DynamicReadaheadConfig {
|
||||||
|
maxReadAheadMediaTimeMs: number;
|
||||||
|
minReadAheadMediaTimeMs: number;
|
||||||
|
readAheadGrowthRateMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamSelectionConfig {
|
||||||
|
maxBitrate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebPlayerConfig {
|
||||||
|
useCobaltTvosDash: boolean;
|
||||||
|
webPlayerActionsPorting: WebPlayerActionsPorting;
|
||||||
|
gatewayExperimentGroup: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebPlayerActionsPorting {
|
||||||
|
subscribeCommand: SubscribeCommand;
|
||||||
|
unsubscribeCommand: UnsubscribeCommand;
|
||||||
|
addToWatchLaterCommand: AddToWatchLaterCommand;
|
||||||
|
removeFromWatchLaterCommand: RemoveFromWatchLaterCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddToWatchLaterCommand {
|
||||||
|
clickTrackingParams: string;
|
||||||
|
playlistEditEndpoint: AddToWatchLaterCommandPlaylistEditEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddToWatchLaterCommandPlaylistEditEndpoint {
|
||||||
|
playlistId: string;
|
||||||
|
actions: PurpleAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurpleAction {
|
||||||
|
addedVideoId: string;
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveFromWatchLaterCommand {
|
||||||
|
clickTrackingParams: string;
|
||||||
|
playlistEditEndpoint: RemoveFromWatchLaterCommandPlaylistEditEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveFromWatchLaterCommandPlaylistEditEndpoint {
|
||||||
|
playlistId: string;
|
||||||
|
actions: FluffyAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FluffyAction {
|
||||||
|
action: string;
|
||||||
|
removedVideoId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscribeCommand {
|
||||||
|
clickTrackingParams: string;
|
||||||
|
subscribeEndpoint: SubscribeEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscribeEndpoint {
|
||||||
|
channelIds: string[];
|
||||||
|
params: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnsubscribeCommand {
|
||||||
|
clickTrackingParams: string;
|
||||||
|
unsubscribeEndpoint: SubscribeEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseContext {
|
||||||
|
serviceTrackingParams: ServiceTrackingParam[];
|
||||||
|
maxAgeSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceTrackingParam {
|
||||||
|
service: string;
|
||||||
|
params: Param[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Param {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Storyboards {
|
||||||
|
playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStoryboardSpecRenderer {
|
||||||
|
spec: string;
|
||||||
|
recommendedLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamingData {
|
||||||
|
expiresInSeconds: string;
|
||||||
|
formats: Format[];
|
||||||
|
adaptiveFormats: AdaptiveFormat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdaptiveFormat {
|
||||||
|
itag: number;
|
||||||
|
mimeType: string;
|
||||||
|
bitrate: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
initRange: Range;
|
||||||
|
indexRange: Range;
|
||||||
|
lastModified: string;
|
||||||
|
contentLength: string;
|
||||||
|
quality: string;
|
||||||
|
fps?: number;
|
||||||
|
qualityLabel?: string;
|
||||||
|
projectionType: ProjectionType;
|
||||||
|
averageBitrate: number;
|
||||||
|
approxDurationMs: string;
|
||||||
|
signatureCipher: string;
|
||||||
|
colorInfo?: ColorInfo;
|
||||||
|
highReplication?: boolean;
|
||||||
|
audioQuality?: string;
|
||||||
|
audioSampleRate?: string;
|
||||||
|
audioChannels?: number;
|
||||||
|
loudnessDb?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorInfo {
|
||||||
|
primaries: string;
|
||||||
|
transferCharacteristics: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Range {
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProjectionType {
|
||||||
|
Rectangular = 'RECTANGULAR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
itag: number;
|
||||||
|
mimeType: string;
|
||||||
|
bitrate: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
lastModified: string;
|
||||||
|
quality: string;
|
||||||
|
fps: number;
|
||||||
|
qualityLabel: string;
|
||||||
|
projectionType: ProjectionType;
|
||||||
|
audioQuality: string;
|
||||||
|
approxDurationMs: string;
|
||||||
|
audioSampleRate: string;
|
||||||
|
audioChannels: number;
|
||||||
|
signatureCipher: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPlayerResponseVideoDetails {
|
||||||
|
videoId: string;
|
||||||
|
title: string;
|
||||||
|
lengthSeconds: string;
|
||||||
|
channelId: string;
|
||||||
|
isOwnerViewing: boolean;
|
||||||
|
isCrawlable: boolean;
|
||||||
|
thumbnail: ImageClass;
|
||||||
|
allowRatings: boolean;
|
||||||
|
viewCount: string;
|
||||||
|
author: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
isUnpluggedCorpus: boolean;
|
||||||
|
musicVideoType: string;
|
||||||
|
isLiveContent: boolean;
|
||||||
|
elapsedSeconds: number;
|
||||||
|
isPaused: boolean;
|
||||||
|
|
||||||
|
// youtube-music only
|
||||||
|
album?: string | null;
|
||||||
|
}
|
||||||
189
types/youtube-player.ts
Normal file
189
types/youtube-player.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// TODO: fully type definitions for youtube-player
|
||||||
|
|
||||||
|
import { GetPlayerResponse } from './get-player-response';
|
||||||
|
|
||||||
|
export interface YoutubePlayer {
|
||||||
|
getInternalApiInterface: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getApiInterface: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
cueVideoByPlayerVars: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
loadVideoByPlayerVars: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
preloadVideoByPlayerVars: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAdState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
sendAbandonmentPing: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setLoopRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getLoopRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setAutonavState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
seekToLiveHead: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
requestSeekToWallTimeSeconds: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
seekToStreamTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
startSeekCsiAction: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStreamTimeOffset: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setInlinePreview: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateDownloadState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
queueOfflineAction: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
pauseVideoDownload: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
resumeVideoDownload: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
refreshAllStaleEntities: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isOrchestrationLeader: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAppState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateLastActiveTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setBlackout: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setUserEngagement: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateSubtitlesUserSettings: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPresentingPlayerType: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
canPlayType: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updatePlaylist: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateVideoData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateEnvironmentData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
sendVideoStatsEngageEvent: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
productsInVideoVisibilityUpdated: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setSafetyMode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isAtLiveHead: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoAspectRatio: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPreferredQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaybackQualityLabel: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setPlaybackQualityRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
onAdUxClicked: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getFeedbackProductData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStoryboardFrame: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStoryboardFrameIndex: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStoryboardLevel: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getNumberOfStoryboardLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getCaptionWindowContainerId: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAvailableQualityLabels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
addUtcCueRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
showAirplayPicker: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
dispatchReduxAction: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlayerResponse: () => GetPlayerResponse;
|
||||||
|
getHeartbeatResponse: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
changeMarkerVisibility: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setAutonav: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isNotServable: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
channelSubscribed: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
channelUnsubscribed: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
togglePictureInPicture: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
supportsGaplessAudio: () => boolean;
|
||||||
|
supportsGaplessShorts: () => boolean;
|
||||||
|
enqueueVideoByPlayerVars: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
clearQueue: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAudioTrack: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setAudioTrack: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAvailableAudioTracks: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getMaxPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getUserPlaybackQualityPreference: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getSubtitlesUserSettings: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
resetSubtitlesUserSettings: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setMinimized: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setOverlayVisibility: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
confirmYpcRental: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
toggleSubtitlesOn: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isSubtitlesOn: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
queueNextVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
handleExternalCall: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
logApiCall: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isExternalMethodAvailable: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setScreenLayer: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getCurrentPlaylistSequence: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaylistSequenceForTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
shouldSendVisibilityState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
syncVolume: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
highlightSettingsMenuItem: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
openSettingsMenuItem: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVisibilityState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isMutedByMutedAutoplay: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setGlobalCrop: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setInternalSize: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
seekBy: (seconds: number) => void;
|
||||||
|
showControls: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
hideControls: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
cancelPlayback: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getProgressState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isInline: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setInline: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setLoopVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getLoopVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoContentRect: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoStats: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStoryboardFormat: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
toggleFullscreen: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isFullscreen: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlayerSize: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
toggleSubtitles: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setCenterCrop: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setFauxFullscreen: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setSizeStyle: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
handleGlobalKeyDown: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
handleGlobalKeyUp: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
wakeUpControls: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
cueVideoById: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
loadVideoById: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
cueVideoByUrl: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
loadVideoByUrl: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
playVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
pauseVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
stopVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
clearVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoBytesLoaded: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoBytesTotal: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoLoadedFraction: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoStartBytes: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
cuePlaylist: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
loadPlaylist: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
nextVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
previousVideo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
playVideoAt: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setShuffle: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setLoop: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaylist: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaylistIndex: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaylistId: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
loadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
unloadModule: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setOption: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getOption: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getOptions: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
mute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
unMute: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isMuted: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setVolume: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVolume: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
seekTo: (seconds: number) => void;
|
||||||
|
getPlayerMode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlayerState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAvailablePlaybackRates: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setPlaybackQuality: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAvailableQualityLevels: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getCurrentTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getDuration: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
addEventListener: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
removeEventListener: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getDebugText: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
addCueRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
removeCueRange: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setSize: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
destroy: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getSphericalProperties: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setSphericalProperties: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
mutedAutoplay: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoEmbedCode: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getVideoUrl: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getMediaReferenceTime: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getSize: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
logImaAdEvent: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
preloadVideoById: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setAccountLinkState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateAccountLinkingConfig: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getAvailableQualityData: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setCompositeParam: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getStatsForNerds: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
showVideoInfo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
hideVideoInfo: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
isVideoInfoVisible: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
getPlaybackRate: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
setPlaybackRate: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateFullerscreenEduButtonSubtleModeState: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
updateFullerscreenEduButtonVisibility: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
addEmbedsConversionTrackingParams: <Parameters extends unknown[], Return>(...params: Parameters) => Return;
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
const isTesting = () => process.env.NODE_ENV === 'test';
|
|
||||||
|
|
||||||
module.exports = { isTesting };
|
|
||||||
3
utils/testing.ts
Normal file
3
utils/testing.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const isTesting = () => process.env.NODE_ENV === 'test';
|
||||||
|
|
||||||
|
export default { isTesting };
|
||||||
5
utils/type-utils.ts
Normal file
5
utils/type-utils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type Entries<T> = {
|
||||||
|
[K in keyof T]: [K, T[K]];
|
||||||
|
}[keyof T][];
|
||||||
|
|
||||||
|
export type ValueOf<T> = T[keyof T];
|
||||||
Reference in New Issue
Block a user