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

View File

@ -157,7 +157,6 @@
"electron-store": "8.1.0",
"electron-unhandled": "4.0.1",
"electron-updater": "6.1.4",
"eslint-import-resolver-typescript": "^3.6.1",
"fast-average-color": "9.4.0",
"fast-equals": "^5.0.1",
"filenamify": "6.0.0",
@ -180,7 +179,7 @@
"@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.11",
"@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",
"builtin-modules": "^3.3.0",
"cross-env": "7.0.3",
@ -189,7 +188,9 @@
"electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0",
"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-prettier": "5.0.1",
"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 { deepmerge } from 'deepmerge-ts';
import { mainPlugins } from 'virtual:plugins';
import { PluginDef } from '@/types/plugins';
import { PluginConfig, PluginDef } from '@/types/plugins';
import { BackendContext } from '@/types/contexts';
import config from '@/config';
import { startPlugin, stopPlugin } from '@/utils';
@ -14,35 +12,37 @@ const loadedPluginMap: Record<string, PluginDef> = {};
const createContext = (id: string, win: BrowserWindow): BackendContext => ({
getConfig: () =>
// @ts-expect-error ts dum dum
deepmerge(
mainPlugins[id].config,
config.get(`plugins.${id}`) ?? { enabled: false },
),
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig);
},
send: (event: string, ...args: unknown[]) => {
win.webContents.send(event, ...args);
},
// @ts-expect-error ts dum dum
handle: (event: string, listener) => {
// @ts-expect-error ts dum dum
ipcMain.handle(event, (_, ...args) => listener(...(args as never)));
},
// @ts-expect-error ts dum dum
on: (event: string, listener) => {
// @ts-expect-error ts dum dum
ipcMain.on(event, (_, ...args) => listener(...(args as never)));
ipc: {
send: (event: string, ...args: unknown[]) => {
win.webContents.send(event, ...args);
},
handle: (event: string, listener: CallableFunction) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
},
on: (event: string, listener: CallableFunction) => {
ipcMain.on(event, (_, ...args: unknown[]) => {
listener(...args);
});
},
},
window: win,
});
export const forceUnloadMainPlugin = async (
id: string,
win: BrowserWindow,
): Promise<void> => {
const plugin = loadedPluginMap[id]!;
const plugin = loadedPluginMap[id];
if (!plugin) return;
return new Promise<void>((resolve, reject) => {

View File

@ -15,6 +15,17 @@ const createContext = (id: string): RendererContext => ({
setConfig: async (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) => {
@ -27,7 +38,7 @@ export const forceUnloadRendererPlugin = (id: string) => {
if (!plugin) return;
stopPlugin(id, plugin, { ctx: 'renderer', context: createContext(id) });
if (plugin.renderer?.stylesheet)
if (plugin?.stylesheets)
document.querySelector(`style#plugin-${id}`)?.remove();
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
@ -42,14 +53,14 @@ export const forceLoadRendererPlugin = (id: string) => {
context: createContext(id),
});
if (hasEvaled || plugin.renderer?.stylesheet) {
if (hasEvaled || plugin?.stylesheets) {
loadedPluginMap[id] = plugin;
if (plugin.renderer?.stylesheet)
if (plugin?.stylesheets)
document.head.appendChild(
Object.assign(document.createElement('style'), {
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({
name: 'Blur Navigation Bar',
renderer: { stylesheet: style },
renderer: { stylesheets: [style] },
});

View File

@ -1,9 +1,5 @@
import { rendererPlugins } from 'virtual:plugins';
import {
PluginBaseConfig,
PluginBuilder,
RendererPluginFactory,
} from '@/plugins/utils/builder';
import { startingPages } from './providers/extracted-data';
@ -81,6 +77,13 @@ function onApiLoaded() {
{ passive: true },
);
Object.values(getAllLoadedRendererPlugins())
.forEach((plugin) => {
if (typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api!);
}
});
window.ipcRenderer.send('ytmd:player-api-loaded');
// Navigate to "Starting page"
@ -133,6 +136,9 @@ function onApiLoaded() {
forceLoadRendererPlugin(id);
if (api) {
const plugin = getLoadedRendererPlugin(id);
if (plugin && typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api);
}
}
},
);
@ -141,6 +147,9 @@ function onApiLoaded() {
'config-changed',
(_event, id: string, newConfig: PluginBaseConfig) => {
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';
export interface BackendContext {
export interface BaseContext {
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
}
export interface BackendContext extends BaseContext {
ipc: {
send: (event: string, ...args: unknown[]) => void;
handle: (event: string, listener: CallableFunction) => void;
on: (event: string, listener: CallableFunction) => void;
};
win: BrowserWindow;
electron: typeof import('electron');
window: BrowserWindow;
}
export interface MenuContext {
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
export interface MenuContext extends BaseContext {
window: BrowserWindow;
refresh: () => Promise<void> | void;
}
export interface PreloadContext {
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
}
export interface PreloadContext extends BaseContext {}
export interface RendererContext {
getConfig(): PluginConfig;
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
export interface RendererContext extends BaseContext {
ipc: {
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 {
BackendContext,
MenuContext,
@ -11,15 +13,17 @@ export type PluginConfig = {
enabled: boolean;
} & Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PluginExtra = Record<string, any>;
type PluginExtra = Record<string, unknown>;
type PluginLifecycle<T> =
| ((ctx: T) => void | Promise<void>)
| ({
start?(ctx: T): void | Promise<void>;
stop?(ctx: T): void | Promise<void>;
} & PluginExtra);
export type PluginLifecycleSimple<T> = (ctx: T) => void | Promise<void>;
export type PluginLifecycleExtra<T> = {
start?: PluginLifecycleSimple<T>;
stop?: PluginLifecycleSimple<T>;
onConfigChange?: (newConfig: PluginConfig) => void | Promise<void>;
onPlayerApiReady?: (playerApi: YoutubePlayer) => void | Promise<void>;
} & PluginExtra;
export type PluginLifecycle<T> = PluginLifecycleSimple<T> | PluginLifecycleExtra<T>;
export interface PluginDef {
name: string;
@ -28,11 +32,10 @@ export interface PluginDef {
config: PluginConfig;
menu?: (ctx: MenuContext) => Electron.MenuItemConstructorOptions[];
stylesheets?: string[];
restartNeeded?: boolean;
backend?: PluginLifecycle<BackendContext>;
preload?: PluginLifecycle<PreloadContext>;
renderer?: PluginLifecycle<RendererContext> & {
stylesheet?: string;
};
renderer?: PluginLifecycle<RendererContext>;
}

View File

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

View File

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