mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-09 17:51:46 +00:00
16
.eslintrc.js
16
.eslintrc.js
@ -2,14 +2,26 @@ module.exports = {
|
||||
extends: [
|
||||
'eslint: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: {
|
||||
ecmaVersion: 'latest',
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
'arrow-parens': ['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/newline-after-import': 'error',
|
||||
'import/no-default-export': 'off',
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
/dist
|
||||
/pack
|
||||
electron-builder.yml
|
||||
.vscode/settings.json
|
||||
.idea
|
||||
|
||||
@ -1,22 +1,38 @@
|
||||
export interface WindowSizeConfig {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
'window-size': {
|
||||
width: 1100,
|
||||
height: 550,
|
||||
},
|
||||
'window-maximized': false,
|
||||
'window-position': {
|
||||
x: -1,
|
||||
y: -1,
|
||||
},
|
||||
'url': 'https://music.youtube.com',
|
||||
'options': {
|
||||
tray: false,
|
||||
appVisible: true,
|
||||
autoUpdates: true,
|
||||
alwaysOnTop: false,
|
||||
hideMenu: false,
|
||||
hideMenuWarned: false,
|
||||
startAtLogin: false,
|
||||
disableHardwareAcceleration: false,
|
||||
removeUpgradeButton: false,
|
||||
restartOnConfigChanges: false,
|
||||
trayClickPlayPause: false,
|
||||
autoResetAppCache: false,
|
||||
resumeOnStart: true,
|
||||
likeButtons: '',
|
||||
proxy: '',
|
||||
startingPage: '',
|
||||
overrideUserAgent: false,
|
||||
themes: {} as string[],
|
||||
},
|
||||
'plugins': {
|
||||
// Enabled plugins
|
||||
@ -26,7 +42,9 @@ const defaultConfig = {
|
||||
'adblocker': {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
blocker: 'In player',
|
||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||
disableDefaultLists: [],
|
||||
},
|
||||
// Disabled plugins
|
||||
'shortcuts': {
|
||||
@ -92,11 +110,14 @@ const defaultConfig = {
|
||||
forceHide: false,
|
||||
},
|
||||
'picture-in-picture': {
|
||||
enabled: false,
|
||||
alwaysOnTop: true,
|
||||
savePosition: true,
|
||||
saveSize: false,
|
||||
hotkey: 'P',
|
||||
'enabled': false,
|
||||
'alwaysOnTop': true,
|
||||
'savePosition': true,
|
||||
'saveSize': false,
|
||||
'hotkey': 'P',
|
||||
'pip-position': [10, 10],
|
||||
'pip-size': [450, 275],
|
||||
'isInPiP': false,
|
||||
},
|
||||
'captions-selector': {
|
||||
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}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
}
|
||||
};
|
||||
|
||||
const migrations = {
|
||||
'>=1.20.0'(store) {
|
||||
'>=1.20.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'visualizer');
|
||||
|
||||
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
||||
@ -25,14 +26,14 @@ const migrations = {
|
||||
store.set('options.likeButtons', 'force');
|
||||
}
|
||||
},
|
||||
'>=1.17.0'(store) {
|
||||
'>=1.17.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'picture-in-picture');
|
||||
|
||||
if (store.get('plugins.video-toggle.mode') === undefined) {
|
||||
store.set('plugins.video-toggle.mode', 'custom');
|
||||
}
|
||||
},
|
||||
'>=1.14.0'(store) {
|
||||
'>=1.14.0'(store: Conf<Record<string, unknown>>) {
|
||||
if (
|
||||
typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
|
||||
) {
|
||||
@ -44,18 +45,25 @@ const migrations = {
|
||||
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) {
|
||||
store.set('plugins.discord.listenAlong', true);
|
||||
}
|
||||
},
|
||||
'>=1.12.0'(store) {
|
||||
const options = store.get('plugins.shortcuts');
|
||||
'>=1.12.0'(store: Conf<Record<string, unknown>>) {
|
||||
const options = store.get('plugins.shortcuts') as Record<string, {
|
||||
action: string;
|
||||
shortcut: unknown;
|
||||
}[] | Record<string, unknown>>;
|
||||
let updated = false;
|
||||
for (const optionType of ['global', 'local']) {
|
||||
if (Array.isArray(options[optionType])) {
|
||||
const updatedOptions = {};
|
||||
for (const optionObject of options[optionType]) {
|
||||
const optionsArray = options[optionType] as {
|
||||
action: string;
|
||||
shortcut: unknown;
|
||||
}[];
|
||||
const updatedOptions: Record<string, unknown> = {};
|
||||
for (const optionObject of optionsArray) {
|
||||
if (optionObject.action && optionObject.shortcut) {
|
||||
updatedOptions[optionObject.action] = optionObject.shortcut;
|
||||
}
|
||||
@ -70,20 +78,21 @@ const migrations = {
|
||||
store.set('plugins.shortcuts', options);
|
||||
}
|
||||
},
|
||||
'>=1.11.0'(store) {
|
||||
'>=1.11.0'(store: Conf<Record<string, unknown>>) {
|
||||
if (store.get('options.resumeOnStart') === undefined) {
|
||||
store.set('options.resumeOnStart', true);
|
||||
}
|
||||
},
|
||||
'>=1.7.0'(store) {
|
||||
const enabledPlugins = store.get('plugins');
|
||||
'>=1.7.0'(store: Conf<Record<string, unknown>>) {
|
||||
const enabledPlugins = store.get('plugins') as string[];
|
||||
if (!Array.isArray(enabledPlugins)) {
|
||||
console.warn('Plugins are not in array format, cannot migrate');
|
||||
return;
|
||||
}
|
||||
|
||||
// Include custom options
|
||||
const plugins = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const plugins: Record<string, any> = {
|
||||
adblocker: {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
@ -95,7 +104,9 @@ const migrations = {
|
||||
downloadFolder: undefined, // Custom download folder (absolute path)
|
||||
},
|
||||
};
|
||||
|
||||
for (const enabledPlugin of enabledPlugins) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
plugins[enabledPlugin] = {
|
||||
...plugins[enabledPlugin],
|
||||
enabled: true,
|
||||
@ -106,7 +117,7 @@ const migrations = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = new Store({
|
||||
export default new Store({
|
||||
defaults,
|
||||
clearInvalidConfig: false,
|
||||
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';
|
||||
const path = require('node:path');
|
||||
import path from 'node:path';
|
||||
|
||||
const electron = require('electron');
|
||||
const enhanceWebRequest = require('electron-better-web-request').default;
|
||||
const is = require('electron-is');
|
||||
const unhandled = require('electron-unhandled');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
import electron, { BrowserWindow } from 'electron';
|
||||
import enhanceWebRequest from 'electron-better-web-request';
|
||||
import is from 'electron-is';
|
||||
import unhandled from 'electron-unhandled';
|
||||
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
|
||||
unhandled({
|
||||
@ -27,7 +30,7 @@ process.env.NODE_OPTIONS = '';
|
||||
|
||||
const { app } = electron;
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
let mainWindow: Electron.BrowserWindow | null;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@ -45,7 +48,7 @@ if (config.get('options.disableHardwareAcceleration')) {
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@ -54,7 +57,7 @@ if (config.get('options.proxy')) {
|
||||
}
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')({
|
||||
electronDebug({
|
||||
showDevTools: false, // Disable automatic devTools on new window
|
||||
});
|
||||
|
||||
@ -67,15 +70,14 @@ if (process.platform === 'win32') {
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
// For multiple windows store them in an array
|
||||
// For multiple Windows store them in an array
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
/** @param {Electron.BrowserWindow} win */
|
||||
function loadPlugins(win) {
|
||||
function loadPlugins(win: BrowserWindow) {
|
||||
injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
|
||||
// Load user CSS
|
||||
const themes = config.get('options.themes');
|
||||
const themes: string[] = config.get('options.themes');
|
||||
if (Array.isArray(themes)) {
|
||||
for (const cssFile of themes) {
|
||||
fileExists(
|
||||
@ -101,7 +103,8 @@ function loadPlugins(win) {
|
||||
console.log('Loaded plugin - ' + plugin);
|
||||
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -110,7 +113,7 @@ function loadPlugins(win) {
|
||||
function createMainWindow() {
|
||||
const windowSize = config.get('window-size');
|
||||
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 win = new electron.BrowserWindow({
|
||||
@ -120,12 +123,11 @@ function createMainWindow() {
|
||||
backgroundColor: '#000',
|
||||
show: false,
|
||||
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
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegrationInSubFrames: true,
|
||||
affinity: 'main-window', // Main window, and addition windows should work in one process
|
||||
...(isTesting()
|
||||
? undefined
|
||||
: {
|
||||
@ -158,7 +160,7 @@ function createMainWindow() {
|
||||
// Window is offscreen
|
||||
if (is.dev()) {
|
||||
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 {
|
||||
@ -180,10 +182,12 @@ function createMainWindow() {
|
||||
win.webContents.loadURL(urlToLoad);
|
||||
win.on('closed', onClosed);
|
||||
|
||||
type PiPOptions = typeof config.defaultConfig.plugins['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', () => {
|
||||
if (win.isMaximized()) {
|
||||
@ -191,17 +195,18 @@ function createMainWindow() {
|
||||
}
|
||||
|
||||
const position = win.getPosition();
|
||||
const isPiPEnabled
|
||||
const isPiPEnabled: boolean
|
||||
= config.plugins.isEnabled('picture-in-picture')
|
||||
&& config.plugins.getOptions('picture-in-picture').isInPiP;
|
||||
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
|
||||
if (!isPiPEnabled) {
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
let winWasMaximized;
|
||||
let winWasMaximized: boolean;
|
||||
|
||||
win.on('resize', () => {
|
||||
const windowSize = win.getSize();
|
||||
@ -209,7 +214,7 @@ function createMainWindow() {
|
||||
|
||||
const isPiPEnabled
|
||||
= 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) {
|
||||
winWasMaximized = isMaximized;
|
||||
@ -225,14 +230,14 @@ function createMainWindow() {
|
||||
width: windowSize[0],
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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]) {
|
||||
clearTimeout(savedTimeouts[key]);
|
||||
}
|
||||
@ -243,7 +248,7 @@ function createMainWindow() {
|
||||
}, 600);
|
||||
}
|
||||
|
||||
win.webContents.on('render-process-gone', (event, webContents, details) => {
|
||||
app.on('render-process-gone', (event, webContents, details) => {
|
||||
showUnresponsiveDialog(win, details);
|
||||
});
|
||||
|
||||
@ -434,7 +439,7 @@ app.on('ready', () => {
|
||||
autoUpdater.on('update-available', () => {
|
||||
const downloadLink
|
||||
= 'https://github.com/th-ch/youtube-music/releases/latest';
|
||||
const dialogOptions = {
|
||||
const dialogOptions: Electron.MessageBoxOptions = {
|
||||
type: 'info',
|
||||
buttons: ['OK', 'Download', 'Disable updates'],
|
||||
title: 'Application Update',
|
||||
@ -486,13 +491,13 @@ app.on('ready', () => {
|
||||
// Hide the window instead of quitting (quit is available in tray options)
|
||||
if (!forceQuit) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
mainWindow!.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function showUnresponsiveDialog(win, details) {
|
||||
function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProcessGoneDetails) {
|
||||
if (details) {
|
||||
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
||||
}
|
||||
@ -501,7 +506,7 @@ function showUnresponsiveDialog(win, details) {
|
||||
type: 'error',
|
||||
title: 'Window 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'],
|
||||
cancelId: 0,
|
||||
}).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(
|
||||
session = electron.session.defaultSession,
|
||||
session: BetterSession = electron.session.defaultSession as BetterSession,
|
||||
) {
|
||||
// Allows defining multiple "onHeadersReceived" listeners
|
||||
// by enhancing the session.
|
||||
@ -538,15 +545,16 @@ function removeContentSecurityPolicy(
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
});
|
||||
|
||||
type ResolverListener = { apply: () => Record<string, unknown>; context: unknown };
|
||||
// When multiple listeners are defined, apply them all
|
||||
session.webRequest.setResolver('onHeadersReceived', (listeners) => {
|
||||
session.webRequest.setResolver('onHeadersReceived', (listeners: ResolverListener[]) => {
|
||||
return listeners.reduce(
|
||||
async (accumulator, listener) => {
|
||||
(accumulator: Record<string, unknown>, listener: ResolverListener) => {
|
||||
if (accumulator.cancel) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
const result = await listener.apply();
|
||||
const result = listener.apply();
|
||||
return { ...accumulator, ...result };
|
||||
},
|
||||
{ cancel: false },
|
||||
@ -1,24 +1,26 @@
|
||||
const { existsSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const { app, clipboard, Menu, dialog } = require('electron');
|
||||
const is = require('electron-is');
|
||||
const prompt = require('custom-electron-prompt');
|
||||
import is from 'electron-is';
|
||||
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
const { restart } = require('./providers/app-controls');
|
||||
const { getAllPlugins } = require('./plugins/utils');
|
||||
const config = require('./config');
|
||||
const { startingPages } = require('./providers/extracted-data');
|
||||
const promptOptions = require('./providers/prompt-options');
|
||||
import { restart } from './providers/app-controls';
|
||||
import { getAllPlugins } from './plugins/utils';
|
||||
import config from './config';
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
import promptOptions from './providers/prompt-options';
|
||||
|
||||
export type MenuTemplate = (Electron.MenuItemConstructorOptions | Electron.MenuItem)[];
|
||||
|
||||
// True only if in-app-menu was loaded on launch
|
||||
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,
|
||||
type: 'checkbox',
|
||||
checked: config.plugins.isEnabled(plugin),
|
||||
click(item) {
|
||||
click(item: Electron.MenuItem) {
|
||||
if (item.checked) {
|
||||
config.plugins.enable(plugin);
|
||||
} else {
|
||||
@ -26,14 +28,14 @@ const pluginEnabledMenu = (plugin, label = '', hasSubmenu = false, refreshMenu =
|
||||
}
|
||||
|
||||
if (hasSubmenu) {
|
||||
refreshMenu();
|
||||
refreshMenu?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const mainMenuTemplate = (win) => {
|
||||
export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
const refreshMenu = () => {
|
||||
this.setApplicationMenu(win);
|
||||
setApplicationMenu(win);
|
||||
if (inAppMenuActive) {
|
||||
win.webContents.send('refreshMenu');
|
||||
}
|
||||
@ -55,7 +57,9 @@ const mainMenuTemplate = (win) => {
|
||||
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 {
|
||||
label: pluginLabel,
|
||||
submenu: [
|
||||
@ -63,12 +67,11 @@ const mainMenuTemplate = (win) => {
|
||||
{ type: 'separator' },
|
||||
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||
],
|
||||
};
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
}
|
||||
|
||||
return pluginEnabledMenu(plugin);
|
||||
})
|
||||
,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Options',
|
||||
@ -208,7 +211,7 @@ const mainMenuTemplate = (win) => {
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||
...(is.windows() || is.macOS()
|
||||
? // Only works on Win/Mac
|
||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||
@ -222,7 +225,7 @@ const mainMenuTemplate = (win) => {
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||
{
|
||||
label: 'Tray',
|
||||
submenu: [
|
||||
@ -238,8 +241,7 @@ const mainMenuTemplate = (win) => {
|
||||
{
|
||||
label: 'Enabled + app visible',
|
||||
type: 'radio',
|
||||
checked:
|
||||
config.get('options.tray') && config.get('options.appVisible'),
|
||||
checked: !!(config.get('options.tray') && config.get('options.appVisible')),
|
||||
click() {
|
||||
config.setMenuOption('options.tray', true);
|
||||
config.setMenuOption('options.appVisible', true);
|
||||
@ -248,8 +250,7 @@ const mainMenuTemplate = (win) => {
|
||||
{
|
||||
label: 'Enabled + app hidden',
|
||||
type: 'radio',
|
||||
checked:
|
||||
config.get('options.tray') && !config.get('options.appVisible'),
|
||||
checked: !!(config.get('options.tray') && !config.get('options.appVisible')),
|
||||
click() {
|
||||
config.setMenuOption('options.tray', true);
|
||||
config.setMenuOption('options.appVisible', false);
|
||||
@ -320,8 +321,7 @@ const mainMenuTemplate = (win) => {
|
||||
if (webContents.isDevToolsOpened()) {
|
||||
webContents.closeDevTools();
|
||||
} else {
|
||||
const devToolsOptions = {};
|
||||
webContents.openDevTools(devToolsOptions);
|
||||
webContents.openDevTools();
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -384,10 +384,8 @@ const mainMenuTemplate = (win) => {
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
module.exports.mainMenuTemplate = mainMenuTemplate;
|
||||
module.exports.setApplicationMenu = (win) => {
|
||||
const menuTemplate = [...mainMenuTemplate(win)];
|
||||
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||
const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
|
||||
if (process.platform === 'darwin') {
|
||||
const { name } = app;
|
||||
menuTemplate.unshift({
|
||||
@ -396,17 +394,13 @@ module.exports.setApplicationMenu = (win) => {
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideothers' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
selector: 'selectAll:',
|
||||
},
|
||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
|
||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
|
||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
|
||||
{ role: 'selectAll' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' },
|
||||
@ -419,7 +413,7 @@ module.exports.setApplicationMenu = (win) => {
|
||||
Menu.setApplicationMenu(menu);
|
||||
};
|
||||
|
||||
async function setProxy(item, win) {
|
||||
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
||||
const output = await prompt({
|
||||
title: 'Set Proxy',
|
||||
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",
|
||||
"butterchurn": "2.6.7",
|
||||
"butterchurn-presets": "2.4.7",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"custom-electron-titlebar": "4.1.6",
|
||||
"electron-better-web-request": "1.0.1",
|
||||
@ -42,6 +43,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"del-cli": "5.0.1",
|
||||
"electron": "27.0.0-alpha.5",
|
||||
@ -52,7 +56,8 @@
|
||||
"eslint-plugin-prettier": "5.0.0",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "1.37.1"
|
||||
"playwright": "1.37.1",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@ -1128,6 +1133,12 @@
|
||||
"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": {
|
||||
"version": "6.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@ -1250,6 +1267,12 @@
|
||||
"@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": {
|
||||
"version": "1.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
|
||||
@ -1266,6 +1289,231 @@
|
||||
"@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": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.0.22.tgz",
|
||||
@ -1631,6 +1879,15 @@
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
|
||||
@ -2526,6 +2783,19 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@ -3977,18 +4255,6 @@
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
@ -5966,11 +6232,15 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"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": ">=8"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
@ -8271,6 +8541,18 @@
|
||||
"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": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||
@ -8397,16 +8679,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
|
||||
23
package.json
23
package.json
@ -76,17 +76,20 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"directories": {
|
||||
"output": "./pack/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:debug": "DEBUG=pw:browser* playwright test",
|
||||
"start": "electron .",
|
||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
||||
"start": "tsc && electron ./dist/index.js",
|
||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron ./dist/index.js",
|
||||
"generate:package": "node utils/generate-package-json.js",
|
||||
"postinstall": "npm run plugins",
|
||||
"clean": "del-cli dist",
|
||||
"build": "npm run clean && electron-builder --win --mac --linux -p never",
|
||||
"clean": "del-cli dist && del-cli pack",
|
||||
"build": "npm run clean && tsc && electron-builder --win --mac --linux -p never",
|
||||
"build:linux": "npm run clean && electron-builder --linux -p never",
|
||||
"build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never",
|
||||
"build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never",
|
||||
@ -95,11 +98,12 @@
|
||||
"lint": "xo",
|
||||
"changelog": "auto-changelog",
|
||||
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
|
||||
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && 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",
|
||||
"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: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": {
|
||||
"node": ">=16.0.0"
|
||||
@ -114,6 +118,7 @@
|
||||
"browser-id3-writer": "5.0.0",
|
||||
"butterchurn": "2.6.7",
|
||||
"butterchurn-presets": "2.4.7",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"custom-electron-titlebar": "4.1.6",
|
||||
"electron-better-web-request": "1.0.1",
|
||||
@ -143,6 +148,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"del-cli": "5.0.1",
|
||||
"electron": "27.0.0-alpha.5",
|
||||
@ -153,7 +161,8 @@
|
||||
"eslint-plugin-prettier": "5.0.0",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "1.37.1"
|
||||
"playwright": "1.37.1",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"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
|
||||
const path = require('node:path');
|
||||
// Used for caching
|
||||
import path from 'node:path';
|
||||
import { promises } from 'node:fs';
|
||||
|
||||
const { ElectronBlocker } = require('@cliqz/adblocker-electron');
|
||||
import { ElectronBlocker } from '@cliqz/adblocker-electron';
|
||||
|
||||
const SOURCES = [
|
||||
'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',
|
||||
];
|
||||
|
||||
const loadAdBlockerEngine = (
|
||||
session = undefined,
|
||||
export const loadAdBlockerEngine = (
|
||||
session: Electron.Session | undefined = undefined,
|
||||
cache = true,
|
||||
additionalBlockLists = [],
|
||||
disableDefaultLists = false,
|
||||
disableDefaultLists: boolean | string[] = false,
|
||||
) => {
|
||||
// Only use cache if no additional blocklists are passed
|
||||
const cachingOptions
|
||||
@ -56,7 +57,7 @@ const loadAdBlockerEngine = (
|
||||
.catch((error) => console.log('Error loading adBlocker engine', error));
|
||||
};
|
||||
|
||||
module.exports = { loadAdBlockerEngine };
|
||||
export default { loadAdBlockerEngine };
|
||||
if (require.main === module) {
|
||||
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/
|
||||
// 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()) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
require('@cliqz/adblocker-electron-preload');
|
||||
} 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 { ACTIONS, CHANNEL } = require('./actions.js');
|
||||
const { ACTIONS, CHANNEL } = require('./actions.ts');
|
||||
|
||||
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');
|
||||
const { injectCSS } = require('../utils');
|
||||
import { setOptions as setPluginOptions } from '../../config/plugins';
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
import config from '../../config';
|
||||
|
||||
let isInPiP = false;
|
||||
let originalPosition;
|
||||
let originalSize;
|
||||
let originalFullScreen;
|
||||
let originalMaximized;
|
||||
let originalPosition: number[];
|
||||
let originalSize: number[];
|
||||
let originalFullScreen: boolean;
|
||||
let originalMaximized: boolean;
|
||||
|
||||
let win;
|
||||
let options;
|
||||
let win: BrowserWindow;
|
||||
|
||||
// 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 pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
|
||||
|
||||
const setLocalOptions = (_options) => {
|
||||
const setLocalOptions = (_options: Partial<PiPOptions>) => {
|
||||
options = { ...options, ..._options };
|
||||
setOptions('picture-in-picture', _options);
|
||||
setPluginOptions('picture-in-picture', _options);
|
||||
};
|
||||
|
||||
const togglePiP = async () => {
|
||||
const togglePiP = () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
|
||||
@ -82,7 +89,7 @@ const togglePiP = async () => {
|
||||
win.setWindowButtonVisibility?.(!isInPiP);
|
||||
};
|
||||
|
||||
const blockShortcutsInPiP = (event, input) => {
|
||||
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === 'f') {
|
||||
@ -93,14 +100,14 @@ const blockShortcutsInPiP = (event, input) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = (_win, _options) => {
|
||||
export default (_win: BrowserWindow, _options: PiPOptions) => {
|
||||
options ??= _options;
|
||||
win ??= _win;
|
||||
setLocalOptions({ isInPiP });
|
||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||
ipcMain.on('picture-in-picture', async () => {
|
||||
await togglePiP();
|
||||
ipcMain.on('picture-in-picture', () => {
|
||||
togglePiP();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.setOptions = setLocalOptions;
|
||||
export const setOptions = setLocalOptions;
|
||||
@ -1,6 +1,6 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { toKeyEvent } = require('keyboardevent-from-electron-accelerator');
|
||||
const keyEventAreEqual = require('keyboardevents-areequal');
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { toKeyEvent } from 'keyboardevent-from-electron-accelerator';
|
||||
import keyEventAreEqual from 'keyboardevents-areequal';
|
||||
|
||||
const { getSongMenu } = require('../../providers/dom-elements');
|
||||
const { ElementFromFile, templatePath } = require('../utils');
|
||||
@ -1,6 +1,6 @@
|
||||
const prompt = require('custom-electron-prompt');
|
||||
|
||||
const { setOptions } = require('./back.js');
|
||||
const { setOptions } = require('./back.ts');
|
||||
|
||||
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');
|
||||
const is = require('electron-is');
|
||||
import { ipcRenderer } from 'electron';
|
||||
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 $ = 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]) => {
|
||||
const preloadPath = await ipcRenderer.invoke(
|
||||
@ -20,9 +27,10 @@ plugins.forEach(async ([plugin, options]) => {
|
||||
'plugins',
|
||||
plugin,
|
||||
'preload.js',
|
||||
);
|
||||
) as string;
|
||||
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);
|
||||
});
|
||||
|
||||
@ -32,14 +40,16 @@ plugins.forEach(async ([plugin, options]) => {
|
||||
'plugins',
|
||||
plugin,
|
||||
'actions.js',
|
||||
);
|
||||
) as string;
|
||||
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
|
||||
// contextBridge.exposeInMainWorld(plugin + "Actions", 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',
|
||||
plugin,
|
||||
'front.js',
|
||||
);
|
||||
) as string;
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -69,14 +80,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setupSongControls();
|
||||
|
||||
// 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
|
||||
setInterval(() => window._lact = Date.now(), 900_000);
|
||||
|
||||
// Setup back to front logger
|
||||
if (is.dev()) {
|
||||
ipcRenderer.on('log', (_event, log) => {
|
||||
ipcRenderer.on('log', (_event, log: string) => {
|
||||
console.log(JSON.parse(log));
|
||||
});
|
||||
}
|
||||
@ -100,8 +112,12 @@ function listenForApiLoad() {
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
interface YouTubeMusicAppElement extends HTMLElement {
|
||||
navigate_(page: string): void;
|
||||
}
|
||||
|
||||
function onApiLoaded() {
|
||||
const video = $('video');
|
||||
const video = $('video')!;
|
||||
const audioContext = new AudioContext();
|
||||
const audioSource = audioContext.createMediaElementSource(video);
|
||||
audioSource.connect(audioContext.destination);
|
||||
@ -126,29 +142,29 @@ function onApiLoaded() {
|
||||
);
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
);!
|
||||
|
||||
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||
ipcRenderer.send('apiLoaded');
|
||||
|
||||
// Navigate to "Starting page"
|
||||
const startingPage = config.get('options.startingPage');
|
||||
const startingPage: string = config.get('options.startingPage');
|
||||
if (startingPage && startingPages[startingPage]) {
|
||||
$('ytmusic-app')?.navigate_(startingPages[startingPage]);
|
||||
($('ytmusic-app') as YouTubeMusicAppElement)?.navigate_(startingPages[startingPage]);
|
||||
}
|
||||
|
||||
// Remove upgrade button
|
||||
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) {
|
||||
upgradeButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Hide / Force show like buttons
|
||||
const likeButtonsOptions = config.get('options.likeButtons');
|
||||
const likeButtonsOptions: string = config.get('options.likeButtons');
|
||||
if (likeButtonsOptions) {
|
||||
const likeButtons = $('ytmusic-like-button-renderer');
|
||||
const likeButtons: HTMLElement | null = $('ytmusic-like-button-renderer');
|
||||
if (likeButtons) {
|
||||
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 = {
|
||||
singleton,
|
||||
debounce,
|
||||
cache,
|
||||
throttle,
|
||||
memoize,
|
||||
retry,
|
||||
};
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
function singleton(fn) {
|
||||
export function singleton<T extends (...params: never[]) => unknown>(fn: T): T {
|
||||
let called = false;
|
||||
return (...args) => {
|
||||
|
||||
return ((...args) => {
|
||||
if (called) {
|
||||
return;
|
||||
}
|
||||
|
||||
called = true;
|
||||
return fn(...args);
|
||||
};
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @param {number} delay
|
||||
* @returns {T}
|
||||
*/
|
||||
function debounce(fn, delay) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
export function debounce<T extends (...params: never[]) => unknown>(fn: T, delay: number): T {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return ((...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
function cache(fn) {
|
||||
let lastArgs;
|
||||
let lastResult;
|
||||
return (...args) => {
|
||||
export function cache<T extends (...params: P) => R, P extends never[], R>(fn: T): T {
|
||||
let lastArgs: P;
|
||||
let lastResult: R;
|
||||
return ((...args: P) => {
|
||||
if (
|
||||
args.length !== lastArgs?.length
|
||||
|| args.some((arg, i) => arg !== lastArgs[i])
|
||||
@ -56,22 +32,16 @@ function cache(fn) {
|
||||
}
|
||||
|
||||
return lastResult;
|
||||
};
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/*
|
||||
The following are currently unused, but potentially useful in the future
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @param {number} delay
|
||||
* @returns {T}
|
||||
*/
|
||||
function throttle(fn, delay) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
export function throttle<T extends (...params: unknown[]) => unknown>(fn: T, delay: number): T {
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
return ((...args) => {
|
||||
if (timeout) {
|
||||
return;
|
||||
}
|
||||
@ -80,33 +50,24 @@ function throttle(fn, delay) {
|
||||
timeout = undefined;
|
||||
fn(...args);
|
||||
}, delay);
|
||||
};
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
function memoize(fn) {
|
||||
function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
|
||||
return ((...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (!cache.has(key)) {
|
||||
cache.set(key, fn(...args));
|
||||
}
|
||||
|
||||
return cache.get(key);
|
||||
};
|
||||
return cache.get(key) as unknown;
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
function retry(fn, { retries = 3, delay = 1000 } = {}) {
|
||||
return (...args) => {
|
||||
function retry<T extends (...params: unknown[]) => unknown>(fn: T, { retries = 3, delay = 1000 } = {}): T {
|
||||
return ((...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (error) {
|
||||
@ -117,5 +78,14 @@ function retry(fn, { retries = 3, delay = 1000 } = {}) {
|
||||
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');
|
||||
|
||||
module.exports = { getSongMenu };
|
||||
export default { getSongMenu };
|
||||
@ -1,4 +1,4 @@
|
||||
const startingPages = {
|
||||
export const startingPages: Record<string, string> = {
|
||||
'Default': '',
|
||||
'Home': 'FEmusic_home',
|
||||
'Explore': 'FEmusic_explore',
|
||||
@ -18,6 +18,6 @@ const startingPages = {
|
||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
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({
|
||||
backgroundColor: Color.fromHex('#050505'),
|
||||
minimizable: 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.position = 'fixed';
|
||||
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 customTitlebarPath = path.join(__dirname, 'prompt-custom-titlebar.js');
|
||||
@ -17,4 +17,4 @@ const promptOptions = !is.macOS() && isEnabled('in-app-menu') ? {
|
||||
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
|
||||
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({
|
||||
type: 'keydown',
|
||||
type: 'keyDown',
|
||||
modifiers,
|
||||
keyCode: key,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (win) => {
|
||||
export default (win: BrowserWindow) => {
|
||||
const commands = {
|
||||
// Playback
|
||||
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');
|
||||
const { cache } = require('../providers/decorators');
|
||||
import { cache } from './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
|
||||
/**
|
||||
* @typedef {songInfo} SongInfo
|
||||
*/
|
||||
const songInfo = {
|
||||
export const songInfo: SongInfo = {
|
||||
title: '',
|
||||
artist: '',
|
||||
views: 0,
|
||||
@ -24,11 +39,9 @@ const songInfo = {
|
||||
};
|
||||
|
||||
// Grab the native image using the src
|
||||
const getImage = cache(
|
||||
/**
|
||||
* @returns {Promise<Electron.NativeImage>}
|
||||
*/
|
||||
async (src) => {
|
||||
export const getImage = cache(
|
||||
async (src: string): Promise<Electron.NativeImage> => {
|
||||
|
||||
const result = await net.fetch(src);
|
||||
const buffer = await result.arrayBuffer();
|
||||
const output = nativeImage.createFromBuffer(Buffer.from(buffer));
|
||||
@ -40,8 +53,8 @@ const getImage = cache(
|
||||
},
|
||||
);
|
||||
|
||||
const handleData = async (responseText, win) => {
|
||||
const data = JSON.parse(responseText);
|
||||
const handleData = async (responseText: string, win: Electron.BrowserWindow) => {
|
||||
const data = JSON.parse(responseText) as GetPlayerResponse;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
@ -50,7 +63,7 @@ const handleData = async (responseText, win) => {
|
||||
if (microformat) {
|
||||
songInfo.uploadDate = microformat.uploadDate;
|
||||
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
|
||||
config.set('url', microformat.urlCanonical);
|
||||
}
|
||||
@ -59,8 +72,8 @@ const handleData = async (responseText, win) => {
|
||||
if (videoDetails) {
|
||||
songInfo.title = cleanupName(videoDetails.title);
|
||||
songInfo.artist = cleanupName(videoDetails.author);
|
||||
songInfo.views = videoDetails.viewCount;
|
||||
songInfo.songDuration = videoDetails.lengthSeconds;
|
||||
songInfo.views = Number(videoDetails.viewCount);
|
||||
songInfo.songDuration = Number(videoDetails.lengthSeconds);
|
||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||
songInfo.isPaused = videoDetails.isPaused;
|
||||
songInfo.videoId = videoDetails.videoId;
|
||||
@ -68,33 +81,26 @@ const handleData = async (responseText, win) => {
|
||||
|
||||
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
/**
|
||||
* @callback songInfoCallback
|
||||
* @param {songInfo} songInfo
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @param {songInfoCallback} callback
|
||||
*/
|
||||
const registerCallback = (callback) => {
|
||||
const registerCallback = (callback: SongInfoCallback) => {
|
||||
callbacks.push(callback);
|
||||
};
|
||||
|
||||
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
|
||||
ipcMain.on('video-src-changed', async (_, responseText) => {
|
||||
ipcMain.on('video-src-changed', async (_, responseText: string) => {
|
||||
handlingData = true;
|
||||
await handleData(responseText, win);
|
||||
handlingData = false;
|
||||
@ -102,7 +108,7 @@ const registerProvider = (win) => {
|
||||
c(songInfo, 'video-src-changed');
|
||||
}
|
||||
});
|
||||
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }) => {
|
||||
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }: { isPaused: boolean, elapsedSeconds: number }) => {
|
||||
songInfo.isPaused = isPaused;
|
||||
songInfo.elapsedSeconds = elapsedSeconds;
|
||||
if (handlingData) {
|
||||
@ -122,7 +128,7 @@ const suffixesToRemove = [
|
||||
' (clip officiel)',
|
||||
];
|
||||
|
||||
function cleanupName(name) {
|
||||
export function cleanupName(name: string): string {
|
||||
if (!name) {
|
||||
return name;
|
||||
}
|
||||
@ -138,7 +144,5 @@ function cleanupName(name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
module.exports = registerCallback;
|
||||
module.exports.setupSongInfo = registerProvider;
|
||||
module.exports.getImage = getImage;
|
||||
module.exports.cleanupName = cleanupName;
|
||||
export default registerCallback;
|
||||
export const setupSongInfo = registerProvider;
|
||||
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');
|
||||
const config = require('./config');
|
||||
const getSongControls = require('./providers/song-controls');
|
||||
import { restart } from './providers/app-controls';
|
||||
import config from './config';
|
||||
import getSongControls from './providers/song-controls';
|
||||
|
||||
import type { MenuTemplate } from './menu';
|
||||
|
||||
// Prevent tray being garbage collected
|
||||
let tray: Electron.Tray | undefined;
|
||||
|
||||
/** @type {Electron.Tray} */
|
||||
let tray;
|
||||
type TrayEvent = (event: Electron.KeyboardEvent, bounds: Electron.Rectangle) => void;
|
||||
|
||||
module.exports.setTrayOnClick = (fn) => {
|
||||
export const setTrayOnClick = (fn: TrayEvent) => {
|
||||
if (!tray) {
|
||||
return;
|
||||
}
|
||||
@ -20,8 +22,8 @@ module.exports.setTrayOnClick = (fn) => {
|
||||
tray.on('click', fn);
|
||||
};
|
||||
|
||||
// Wont do anything on macos since its disabled
|
||||
module.exports.setTrayOnDoubleClick = (fn) => {
|
||||
// Won't do anything on macOS since its disabled
|
||||
export const setTrayOnDoubleClick = (fn: TrayEvent) => {
|
||||
if (!tray) {
|
||||
return;
|
||||
}
|
||||
@ -30,7 +32,7 @@ module.exports.setTrayOnDoubleClick = (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')) {
|
||||
tray = undefined;
|
||||
return;
|
||||
@ -63,7 +65,7 @@ module.exports.setUpTray = (app, win) => {
|
||||
}
|
||||
});
|
||||
|
||||
const template = [
|
||||
const template: MenuTemplate = [
|
||||
{
|
||||
label: 'Play/Pause',
|
||||
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