feat: reimplement inject css, fix types

This commit is contained in:
JellyBrick
2023-11-26 23:12:19 +09:00
parent 3ab4cd5d05
commit e12e67af0e
11 changed files with 312 additions and 180 deletions

View File

@ -1,8 +1,8 @@
module.exports = { module.exports = {
extends: [ extends: [
'plugin:import/typescript',
'eslint:recommended', 'eslint:recommended',
'plugin:import/recommended', 'plugin:import/recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
@ -13,51 +13,30 @@ module.exports = {
project: './tsconfig.json', project: './tsconfig.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
sourceType: 'module', sourceType: 'module',
ecmaVersion: 'latest', ecmaVersion: 'latest'
}, },
rules: { rules: {
'arrow-parens': ['error', 'always'], 'arrow-parens': ['error', 'always'],
'object-curly-spacing': ['error', 'always'], 'object-curly-spacing': ['error', 'always'],
'@typescript-eslint/no-floating-promises': 'off', '@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': [ '@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
'off',
{ checksVoidReturn: false },
],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'off', "@typescript-eslint/no-non-null-assertion": "off",
'@typescript-eslint/no-unsafe-assignment': 'off',
'import/first': 'error', 'import/first': 'error',
'import/newline-after-import': 'error', 'import/newline-after-import': 'error',
'import/no-default-export': 'off', 'import/no-default-export': 'off',
'import/no-duplicates': 'error', 'import/no-duplicates': 'error',
'import/no-unresolved': [ 'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
'error',
{
ignore: [
'^virtual:',
'\\?inline$',
'\\?raw$',
'\\?asset&asarUnpack',
'^youtubei.js$',
],
},
],
'import/order': [ 'import/order': [
'error', 'error',
{ {
groups: [ 'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
'builtin',
'external',
['internal', 'index', 'sibling'],
'parent',
'type',
],
'newlines-between': 'always-and-inside-groups', 'newlines-between': 'always-and-inside-groups',
alphabetize: { order: 'ignore', caseInsensitive: false }, 'alphabetize': {order: 'ignore', caseInsensitive: false}
}, }
], ],
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
camelcase: ['error', { properties: 'never' }], 'camelcase': ['error', {properties: 'never'}],
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
'lines-around-comment': [ 'lines-around-comment': [
'error', 'error',
@ -70,21 +49,17 @@ module.exports = {
], ],
'max-len': 'off', 'max-len': 'off',
'no-mixed-operators': 'error', 'no-mixed-operators': 'error',
'no-multi-spaces': ['error', { ignoreEOLComments: true }], 'no-multi-spaces': ['error', {ignoreEOLComments: true}],
'no-tabs': 'error', 'no-tabs': 'error',
'no-void': 'error', 'no-void': 'error',
'no-empty': 'off', 'no-empty': 'off',
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
quotes: [ 'quotes': ['error', 'single', {
'error', avoidEscape: true,
'single', allowTemplateLiterals: false,
{ }],
avoidEscape: true,
allowTemplateLiterals: false,
},
],
'quote-props': ['error', 'consistent'], 'quote-props': ['error', 'consistent'],
semi: ['error', 'always'], 'semi': ['error', 'always'],
}, },
env: { env: {
browser: true, browser: true,
@ -95,12 +70,11 @@ module.exports = {
root: true, root: true,
settings: { settings: {
'import/parsers': { 'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'], '@typescript-eslint/parser': ['.ts']
}, },
'import/resolver': { 'import/resolver': {
typescript: { typescript: {},
alwaysTryTypes: true, exports: {},
},
}, },
}, },
}; };

View File

@ -157,7 +157,6 @@
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.1.4", "electron-updater": "6.1.4",
"eslint-import-resolver-typescript": "^3.6.1",
"fast-average-color": "9.4.0", "fast-average-color": "9.4.0",
"fast-equals": "^5.0.1", "fast-equals": "^5.0.1",
"filenamify": "6.0.0", "filenamify": "6.0.0",
@ -180,7 +179,7 @@
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.11", "@types/howler": "2.2.11",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@typescript-eslint/eslint-plugin": "6.10.0", "@typescript-eslint/eslint-plugin": "6.12.0",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "^3.3.0", "builtin-modules": "^3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
@ -189,7 +188,9 @@
"electron-builder": "24.6.4", "electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "1.0.28", "electron-vite": "1.0.28",
"eslint": "8.53.0", "eslint": "8.54.0",
"eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.0.1",
"glob": "10.3.10", "glob": "10.3.10",

268
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import { deepmerge } from 'deepmerge-ts'; import { deepmerge } from 'deepmerge-ts';
import { mainPlugins } from 'virtual:plugins'; import { mainPlugins } from 'virtual:plugins';
import { PluginDef } from '@/types/plugins'; import { PluginConfig, PluginDef } from '@/types/plugins';
import { BackendContext } from '@/types/contexts'; import { BackendContext } from '@/types/contexts';
import config from '@/config'; import config from '@/config';
import { startPlugin, stopPlugin } from '@/utils'; import { startPlugin, stopPlugin } from '@/utils';
@ -14,35 +12,37 @@ const loadedPluginMap: Record<string, PluginDef> = {};
const createContext = (id: string, win: BrowserWindow): BackendContext => ({ const createContext = (id: string, win: BrowserWindow): BackendContext => ({
getConfig: () => getConfig: () =>
// @ts-expect-error ts dum dum
deepmerge( deepmerge(
mainPlugins[id].config, mainPlugins[id].config,
config.get(`plugins.${id}`) ?? { enabled: false }, config.get(`plugins.${id}`) ?? { enabled: false },
), ) as PluginConfig,
setConfig: (newConfig) => { setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig); config.setPartial(`plugins.${id}`, newConfig);
}, },
send: (event: string, ...args: unknown[]) => { ipc: {
win.webContents.send(event, ...args); send: (event: string, ...args: unknown[]) => {
}, win.webContents.send(event, ...args);
// @ts-expect-error ts dum dum },
handle: (event: string, listener) => { handle: (event: string, listener: CallableFunction) => {
// @ts-expect-error ts dum dum // eslint-disable-next-line @typescript-eslint/no-unsafe-return
ipcMain.handle(event, (_, ...args) => listener(...(args as never))); ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
}, },
// @ts-expect-error ts dum dum on: (event: string, listener: CallableFunction) => {
on: (event: string, listener) => { ipcMain.on(event, (_, ...args: unknown[]) => {
// @ts-expect-error ts dum dum listener(...args);
ipcMain.on(event, (_, ...args) => listener(...(args as never))); });
},
}, },
window: win,
}); });
export const forceUnloadMainPlugin = async ( export const forceUnloadMainPlugin = async (
id: string, id: string,
win: BrowserWindow, win: BrowserWindow,
): Promise<void> => { ): Promise<void> => {
const plugin = loadedPluginMap[id]!; const plugin = loadedPluginMap[id];
if (!plugin) return; if (!plugin) return;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {

View File

@ -15,6 +15,17 @@ const createContext = (id: string): RendererContext => ({
setConfig: async (newConfig) => { setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('set-config', id, newConfig); await window.ipcRenderer.invoke('set-config', id, newConfig);
}, },
ipc: {
send: (event: string, ...args: unknown[]) => {
window.ipcRenderer.send(event, ...args);
},
invoke: (event: string, ...args: unknown[]) => window.ipcRenderer.invoke(event, ...args),
on: (event: string, listener: CallableFunction) => {
window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
listener(...args);
});
},
},
}); });
export const forceUnloadRendererPlugin = (id: string) => { export const forceUnloadRendererPlugin = (id: string) => {
@ -27,7 +38,7 @@ export const forceUnloadRendererPlugin = (id: string) => {
if (!plugin) return; if (!plugin) return;
stopPlugin(id, plugin, { ctx: 'renderer', context: createContext(id) }); stopPlugin(id, plugin, { ctx: 'renderer', context: createContext(id) });
if (plugin.renderer?.stylesheet) if (plugin?.stylesheets)
document.querySelector(`style#plugin-${id}`)?.remove(); document.querySelector(`style#plugin-${id}`)?.remove();
console.log('[YTMusic]', `"${id}" plugin is unloaded`); console.log('[YTMusic]', `"${id}" plugin is unloaded`);
@ -42,14 +53,14 @@ export const forceLoadRendererPlugin = (id: string) => {
context: createContext(id), context: createContext(id),
}); });
if (hasEvaled || plugin.renderer?.stylesheet) { if (hasEvaled || plugin?.stylesheets) {
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
if (plugin.renderer?.stylesheet) if (plugin?.stylesheets)
document.head.appendChild( document.head.appendChild(
Object.assign(document.createElement('style'), { Object.assign(document.createElement('style'), {
id: `plugin-${id}`, id: `plugin-${id}`,
innerHTML: plugin.renderer?.stylesheet ?? '', innerHTML: plugin?.stylesheets ?? '',
}), }),
); );

View File

@ -3,5 +3,5 @@ import style from './style.css?inline';
export default createPlugin({ export default createPlugin({
name: 'Blur Navigation Bar', name: 'Blur Navigation Bar',
renderer: { stylesheet: style }, renderer: { stylesheets: [style] },
}); });

View File

@ -1,9 +1,5 @@
import { rendererPlugins } from 'virtual:plugins';
import { import {
PluginBaseConfig, PluginBaseConfig,
PluginBuilder,
RendererPluginFactory,
} from '@/plugins/utils/builder'; } from '@/plugins/utils/builder';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
@ -81,6 +77,13 @@ function onApiLoaded() {
{ passive: true }, { passive: true },
); );
Object.values(getAllLoadedRendererPlugins())
.forEach((plugin) => {
if (typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api!);
}
});
window.ipcRenderer.send('ytmd:player-api-loaded'); window.ipcRenderer.send('ytmd:player-api-loaded');
// Navigate to "Starting page" // Navigate to "Starting page"
@ -133,6 +136,9 @@ function onApiLoaded() {
forceLoadRendererPlugin(id); forceLoadRendererPlugin(id);
if (api) { if (api) {
const plugin = getLoadedRendererPlugin(id); const plugin = getLoadedRendererPlugin(id);
if (plugin && typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api);
}
} }
}, },
); );
@ -141,6 +147,9 @@ function onApiLoaded() {
'config-changed', 'config-changed',
(_event, id: string, newConfig: PluginBaseConfig) => { (_event, id: string, newConfig: PluginBaseConfig) => {
const plugin = getAllLoadedRendererPlugins()[id]; const plugin = getAllLoadedRendererPlugins()[id];
if (plugin && typeof plugin.renderer !== 'function') {
plugin.renderer?.onConfigChange?.(newConfig);
}
}, },
); );

View File

@ -1,33 +1,32 @@
import type { IpcMain, IpcRenderer, BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
export interface BackendContext { export interface BaseContext {
getConfig(): PluginConfig; getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void; setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
}
export interface BackendContext extends BaseContext {
ipc: { ipc: {
send: (event: string, ...args: unknown[]) => void;
handle: (event: string, listener: CallableFunction) => void; handle: (event: string, listener: CallableFunction) => void;
on: (event: string, listener: CallableFunction) => void; on: (event: string, listener: CallableFunction) => void;
}; };
win: BrowserWindow; window: BrowserWindow;
electron: typeof import('electron');
} }
export interface MenuContext { export interface MenuContext extends BaseContext {
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
window: BrowserWindow; window: BrowserWindow;
refresh: () => Promise<void> | void; refresh: () => Promise<void> | void;
} }
export interface PreloadContext { export interface PreloadContext extends BaseContext {}
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
}
export interface RendererContext { export interface RendererContext extends BaseContext {
getConfig(): PluginConfig; ipc: {
setConfig(conf: Omit<PluginConfig, 'enabled'>): void; send: (event: string, ...args: unknown[]) => void;
invoke: (event: string, ...args: unknown[]) => Promise<unknown>;
on: (event: string, listener: CallableFunction) => void;
};
} }

View File

@ -1,3 +1,5 @@
import type { YoutubePlayer } from '@/types/youtube-player';
import type { import type {
BackendContext, BackendContext,
MenuContext, MenuContext,
@ -11,15 +13,17 @@ export type PluginConfig = {
enabled: boolean; enabled: boolean;
} & Record<string, unknown>; } & Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any type PluginExtra = Record<string, unknown>;
type PluginExtra = Record<string, any>;
type PluginLifecycle<T> = export type PluginLifecycleSimple<T> = (ctx: T) => void | Promise<void>;
| ((ctx: T) => void | Promise<void>) export type PluginLifecycleExtra<T> = {
| ({ start?: PluginLifecycleSimple<T>;
start?(ctx: T): void | Promise<void>; stop?: PluginLifecycleSimple<T>;
stop?(ctx: T): void | Promise<void>; onConfigChange?: (newConfig: PluginConfig) => void | Promise<void>;
} & PluginExtra); onPlayerApiReady?: (playerApi: YoutubePlayer) => void | Promise<void>;
} & PluginExtra;
export type PluginLifecycle<T> = PluginLifecycleSimple<T> | PluginLifecycleExtra<T>;
export interface PluginDef { export interface PluginDef {
name: string; name: string;
@ -28,11 +32,10 @@ export interface PluginDef {
config: PluginConfig; config: PluginConfig;
menu?: (ctx: MenuContext) => Electron.MenuItemConstructorOptions[]; menu?: (ctx: MenuContext) => Electron.MenuItemConstructorOptions[];
stylesheets?: string[];
restartNeeded?: boolean; restartNeeded?: boolean;
backend?: PluginLifecycle<BackendContext>; backend?: PluginLifecycle<BackendContext>;
preload?: PluginLifecycle<PreloadContext>; preload?: PluginLifecycle<PreloadContext>;
renderer?: PluginLifecycle<RendererContext> & { renderer?: PluginLifecycle<RendererContext>;
stylesheet?: string;
};
} }

View File

@ -1,9 +1,15 @@
import { import type {
BackendContext, BackendContext,
PreloadContext, PreloadContext,
RendererContext, RendererContext,
} from '@/types/contexts'; } from '@/types/contexts';
import { PluginDef, PluginConfig } from '@/types/plugins';
import type {
PluginDef,
PluginConfig,
PluginLifecycleExtra,
PluginLifecycleSimple,
} from '@/types/plugins';
export const createPlugin = ( export const createPlugin = (
def: Omit<PluginDef, 'config'> & { def: Omit<PluginDef, 'config'> & {
@ -17,11 +23,10 @@ type Options =
| { ctx: 'renderer'; context: RendererContext }; | { ctx: 'renderer'; context: RendererContext };
export const startPlugin = (id: string, def: PluginDef, options: Options) => { export const startPlugin = (id: string, def: PluginDef, options: Options) => {
const lifecycle: (ctx: (typeof options)['context']) => void = const lifecycle =
typeof def[options.ctx] === 'function' typeof def[options.ctx] === 'function'
? def[options.ctx] ? def[options.ctx] as PluginLifecycleSimple<Options['context']>
: // @ts-expect-error TS is dum dum : (def[options.ctx] as PluginLifecycleExtra<Options['context']>)?.start;
def[options.ctx]?.start;
if (!lifecycle) return false; if (!lifecycle) return false;
@ -29,7 +34,6 @@ export const startPlugin = (id: string, def: PluginDef, options: Options) => {
const start = performance.now(); const start = performance.now();
lifecycle(options.context); lifecycle(options.context);
// prettier-ignore
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
return true; return true;
@ -43,15 +47,13 @@ export const stopPlugin = (id: string, def: PluginDef, options: Options) => {
if (!def[options.ctx]) return false; if (!def[options.ctx]) return false;
if (typeof def[options.ctx] === 'function') return false; if (typeof def[options.ctx] === 'function') return false;
// @ts-expect-error TS is dum dum const stop = def[options.ctx] as PluginLifecycleExtra<Options['context']>['stop'];
const stop: (ctx: typeof options.context) => void = def[options.ctx]?.stop;
if (!stop) return false; if (!stop) return false;
try { try {
const start = performance.now(); const start = performance.now();
stop(options.context); stop(options.context);
// prettier-ignore
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
return true; return true;

View File

@ -6,13 +6,13 @@ import { Project, ts, ObjectLiteralExpression } from 'ts-morph';
import type { PluginOption } from 'vite'; import type { PluginOption } from 'vite';
export default function (mode: 'backend' | 'preload' | 'renderer' | 'none') { export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): PluginOption {
const pluginFilter = createFilter([ const pluginFilter = createFilter([
'src/plugins/*/index.{js,ts}', 'src/plugins/*/index.{js,ts}',
'src/plugins/*', 'src/plugins/*',
]); ]);
return <PluginOption>{ return {
name: 'ytm-plugin-loader', name: 'ytm-plugin-loader',
async load(id) { async load(id) {
if (!pluginFilter(id)) return null; if (!pluginFilter(id)) return null;
@ -98,7 +98,6 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none') {
return { return {
code: src.getText(), code: src.getText(),
ast: src,
}; };
}, },
}; };