Files
youtube-music/config/dynamic.ts
JellyBrick 82bcadcd64 feat: typescript part 1
Co-authored-by: Su-Yong <simssy2205@gmail.com>
2023-09-03 00:25:48 +09:00

241 lines
7.2 KiB
TypeScript

/* 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);
});
});
}
}
}