mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
change plugin system
This commit is contained in:
66
.eslintrc.js
66
.eslintrc.js
@ -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,30 +13,51 @@ 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': ['off', { checksVoidReturn: false }],
|
'@typescript-eslint/no-misused-promises': [
|
||||||
|
'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': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack', '^youtubei.js$'] }],
|
'import/no-unresolved': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignore: [
|
||||||
|
'^virtual:',
|
||||||
|
'\\?inline$',
|
||||||
|
'\\?raw$',
|
||||||
|
'\\?asset&asarUnpack',
|
||||||
|
'^youtubei.js$',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
'import/order': [
|
'import/order': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
|
groups: [
|
||||||
|
'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',
|
||||||
@ -49,17 +70,21 @@ 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': ['error', 'single', {
|
quotes: [
|
||||||
avoidEscape: true,
|
'error',
|
||||||
allowTemplateLiterals: false,
|
'single',
|
||||||
}],
|
{
|
||||||
|
avoidEscape: true,
|
||||||
|
allowTemplateLiterals: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
'quote-props': ['error', 'consistent'],
|
'quote-props': ['error', 'consistent'],
|
||||||
'semi': ['error', 'always'],
|
semi: ['error', 'always'],
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
@ -67,4 +92,15 @@ module.exports = {
|
|||||||
es6: true,
|
es6: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['dist', 'node_modules'],
|
ignorePatterns: ['dist', 'node_modules'],
|
||||||
|
root: true,
|
||||||
|
settings: {
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ electron-builder.yml
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
.vite-inspect
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
import { defineConfig, defineViteConfig } from 'electron-vite';
|
import { defineConfig, defineViteConfig } from 'electron-vite';
|
||||||
import builtinModules from 'builtin-modules';
|
import builtinModules from 'builtin-modules';
|
||||||
import viteResolve from 'vite-plugin-resolve';
|
import viteResolve from 'vite-plugin-resolve';
|
||||||
|
import Inspect from 'vite-plugin-inspect';
|
||||||
|
|
||||||
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-virtual-module-generator';
|
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer';
|
||||||
|
import pluginLoader from './vite-plugins/plugin-loader';
|
||||||
|
|
||||||
import type { UserConfig } from 'vite';
|
import type { UserConfig } from 'vite';
|
||||||
|
|
||||||
@ -10,10 +14,9 @@ export default defineConfig({
|
|||||||
main: defineViteConfig(({ mode }) => {
|
main: defineViteConfig(({ mode }) => {
|
||||||
const commonConfig: UserConfig = {
|
const commonConfig: UserConfig = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
pluginLoader('backend'),
|
||||||
viteResolve({
|
viteResolve({
|
||||||
'virtual:PluginBuilders': pluginVirtualModuleGenerator('index'),
|
'virtual:plugins': pluginVirtualModuleGenerator('main'),
|
||||||
'virtual:MainPlugins': pluginVirtualModuleGenerator('main'),
|
|
||||||
'virtual:MenuPlugins': pluginVirtualModuleGenerator('menu'),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
publicDir: 'assets',
|
publicDir: 'assets',
|
||||||
@ -31,9 +34,18 @@ export default defineConfig({
|
|||||||
input: './src/index.ts',
|
input: './src/index.ts',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@assets': fileURLToPath(new URL('./assets', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
|
commonConfig.plugins?.push(
|
||||||
|
Inspect({ build: true, outputDir: '.vite-inspect/backend' }),
|
||||||
|
);
|
||||||
return commonConfig;
|
return commonConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,9 +61,9 @@ export default defineConfig({
|
|||||||
preload: defineViteConfig(({ mode }) => {
|
preload: defineViteConfig(({ mode }) => {
|
||||||
const commonConfig: UserConfig = {
|
const commonConfig: UserConfig = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
pluginLoader('preload'),
|
||||||
viteResolve({
|
viteResolve({
|
||||||
'virtual:PluginBuilders': pluginVirtualModuleGenerator('index'),
|
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
|
||||||
'virtual:PreloadPlugins': pluginVirtualModuleGenerator('preload'),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
@ -66,11 +78,20 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||||
input: './src/preload.ts',
|
input: './src/preload.ts',
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@assets': fileURLToPath(new URL('./assets', import.meta.url)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
|
commonConfig.plugins?.push(
|
||||||
|
Inspect({ build: true, outputDir: '.vite-inspect/preload' }),
|
||||||
|
);
|
||||||
return commonConfig;
|
return commonConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +107,9 @@ export default defineConfig({
|
|||||||
renderer: defineViteConfig(({ mode }) => {
|
renderer: defineViteConfig(({ mode }) => {
|
||||||
const commonConfig: UserConfig = {
|
const commonConfig: UserConfig = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
pluginLoader('renderer'),
|
||||||
viteResolve({
|
viteResolve({
|
||||||
'virtual:PluginBuilders': pluginVirtualModuleGenerator('index'),
|
'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
|
||||||
'virtual:RendererPlugins': pluginVirtualModuleGenerator('renderer'),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
root: './src/',
|
root: './src/',
|
||||||
@ -107,9 +128,18 @@ export default defineConfig({
|
|||||||
input: './src/index.html',
|
input: './src/index.html',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@assets': fileURLToPath(new URL('./assets', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
|
commonConfig.plugins?.push(
|
||||||
|
Inspect({ build: true, outputDir: '.vite-inspect/renderer' }),
|
||||||
|
);
|
||||||
return commonConfig;
|
return commonConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,11 +94,12 @@
|
|||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
|
"vite:inspect": "yarpm-pnpm run clean && electron-vite build --mode development && yarpm-pnpm exec serve .vite-inspect",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
|
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
|
||||||
"dev": "electron-vite dev --watch",
|
"dev": "electron-vite dev --watch",
|
||||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run dev",
|
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run dev",
|
||||||
"clean": "del-cli dist && del-cli pack",
|
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||||
"dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
|
"dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
|
||||||
"dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
|
"dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
|
||||||
"dist:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:x64 -p never",
|
"dist:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:x64 -p never",
|
||||||
@ -136,6 +137,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "1.26.11",
|
"@cliqz/adblocker-electron": "1.26.11",
|
||||||
"@cliqz/adblocker-electron-preload": "1.26.11",
|
"@cliqz/adblocker-electron-preload": "1.26.11",
|
||||||
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||||
"@ffmpeg.wasm/main": "0.12.0",
|
"@ffmpeg.wasm/main": "0.12.0",
|
||||||
"@foobar404/wave": "2.0.4",
|
"@foobar404/wave": "2.0.4",
|
||||||
@ -155,6 +157,7 @@
|
|||||||
"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",
|
||||||
@ -164,7 +167,9 @@
|
|||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"node-html-parser": "6.1.11",
|
"node-html-parser": "6.1.11",
|
||||||
"node-id3": "0.2.6",
|
"node-id3": "0.2.6",
|
||||||
|
"serve": "^14.2.1",
|
||||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
|
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
|
||||||
|
"ts-morph": "^20.0.0",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"x11": "2.3.0",
|
"x11": "2.3.0",
|
||||||
"youtubei.js": "7.0.0"
|
"youtubei.js": "7.0.0"
|
||||||
@ -194,6 +199,7 @@
|
|||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"utf-8-validate": "6.0.3",
|
"utf-8-validate": "6.0.3",
|
||||||
"vite": "4.5.0",
|
"vite": "4.5.0",
|
||||||
|
"vite-plugin-inspect": "^0.7.42",
|
||||||
"vite-plugin-resolve": "2.5.1",
|
"vite-plugin-resolve": "2.5.1",
|
||||||
"ws": "8.14.2",
|
"ws": "8.14.2",
|
||||||
"yarpm": "1.2.0"
|
"yarpm": "1.2.0"
|
||||||
|
|||||||
608
pnpm-lock.yaml
generated
608
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,9 @@ import Store from 'electron-store';
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
|
|
||||||
import defaultConfig from './defaults';
|
import defaultConfig from './defaults';
|
||||||
import plugins from './plugins';
|
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import plugins from './plugins';
|
||||||
|
|
||||||
import { restart } from '../providers/app-controls';
|
import { restart } from '../providers/app-controls';
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ const setPartial = (key: string, value: object) => {
|
|||||||
store.set(key, newValue);
|
store.set(key, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
function setMenuOption(key: string, value: unknown) {
|
function setMenuOption(key: string, value: unknown) {
|
||||||
set(key, value);
|
set(key, value);
|
||||||
if (store.get('options.restartOnConfigChanges')) {
|
if (store.get('options.restartOnConfigChanges')) {
|
||||||
restart();
|
restart();
|
||||||
@ -24,24 +25,55 @@ function setMenuOption(key: string, value: unknown) {
|
|||||||
|
|
||||||
// MAGIC OF TYPESCRIPT
|
// MAGIC OF TYPESCRIPT
|
||||||
|
|
||||||
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
type Prev = [
|
||||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
|
never,
|
||||||
type Join<K, P> = K extends string | number ?
|
0,
|
||||||
P extends string | number ?
|
1,
|
||||||
`${K}${'' extends P ? '' : '.'}${P}`
|
2,
|
||||||
: never : never;
|
3,
|
||||||
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
|
4,
|
||||||
{ [K in keyof T]-?: K extends string | number ?
|
5,
|
||||||
`${K}` | Join<K, Paths<T[K], Prev[D]>>
|
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
|
||||||
}[keyof T] : ''
|
: 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 SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
|
type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
|
||||||
type PathValue<T, K extends string> =
|
type PathValue<T, K extends string> = SplitKey<K> extends [
|
||||||
SplitKey<K> extends [infer A extends keyof T, infer B extends string]
|
infer A extends keyof T,
|
||||||
? PathValue<T[A], B>
|
infer B extends string,
|
||||||
: T;
|
]
|
||||||
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) => store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
? PathValue<T[A], B>
|
||||||
|
: T;
|
||||||
|
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
|
||||||
|
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import Conf from 'conf';
|
import Conf from 'conf';
|
||||||
|
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
import { allPlugins } from 'virtual:plugins';
|
||||||
|
|
||||||
import defaults from './defaults';
|
import defaults from './defaults';
|
||||||
|
|
||||||
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
||||||
|
|
||||||
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof pluginBuilders) => {
|
const setDefaultPluginOptions = (
|
||||||
|
store: Conf<Record<string, unknown>>,
|
||||||
|
plugin: string,
|
||||||
|
) => {
|
||||||
if (!store.get(`plugins.${plugin}`)) {
|
if (!store.get(`plugins.${plugin}`)) {
|
||||||
store.set(`plugins.${plugin}`, pluginBuilders[plugin].config);
|
store.set(`plugins.${plugin}`, allPlugins[plugin].config);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,19 +25,24 @@ const migrations = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
|
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
|
||||||
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
|
const originalPreset = store.get('plugins.downloader.preset') as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
if (originalPreset) {
|
if (originalPreset) {
|
||||||
if (originalPreset !== 'opus') {
|
if (originalPreset !== 'opus') {
|
||||||
store.set('plugins.downloader.selectedPreset', 'Custom');
|
store.set('plugins.downloader.selectedPreset', 'Custom');
|
||||||
store.set('plugins.downloader.customPresetSetting', {
|
store.set('plugins.downloader.customPresetSetting', {
|
||||||
extension: 'mp3',
|
extension: 'mp3',
|
||||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
|
ffmpegArgs:
|
||||||
|
(store.get('plugins.downloader.ffmpegArgs') as string[]) ??
|
||||||
|
DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
|
||||||
} satisfies Preset);
|
} satisfies Preset);
|
||||||
} else {
|
} else {
|
||||||
store.set('plugins.downloader.selectedPreset', 'Source');
|
store.set('plugins.downloader.selectedPreset', 'Source');
|
||||||
store.set('plugins.downloader.customPresetSetting', {
|
store.set('plugins.downloader.customPresetSetting', {
|
||||||
extension: null,
|
extension: null,
|
||||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
|
ffmpegArgs:
|
||||||
|
(store.get('plugins.downloader.ffmpegArgs') as string[]) ?? [],
|
||||||
} satisfies Preset);
|
} satisfies Preset);
|
||||||
}
|
}
|
||||||
store.delete('plugins.downloader.preset');
|
store.delete('plugins.downloader.preset');
|
||||||
@ -47,7 +55,7 @@ const migrations = {
|
|||||||
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
||||||
const pluginOptions = store.get('plugins.notifications') || {};
|
const pluginOptions = store.get('plugins.notifications') || {};
|
||||||
store.set('plugins.notifications', {
|
store.set('plugins.notifications', {
|
||||||
...pluginBuilders.notifications.config,
|
...allPlugins.notifications.config,
|
||||||
...pluginOptions,
|
...pluginOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -82,10 +90,14 @@ const migrations = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'>=1.12.0'(store: Conf<Record<string, unknown>>) {
|
'>=1.12.0'(store: Conf<Record<string, unknown>>) {
|
||||||
const options = store.get('plugins.shortcuts') as Record<string, {
|
const options = store.get('plugins.shortcuts') as Record<
|
||||||
action: string;
|
string,
|
||||||
shortcut: unknown;
|
| {
|
||||||
}[] | Record<string, unknown>>;
|
action: string;
|
||||||
|
shortcut: unknown;
|
||||||
|
}[]
|
||||||
|
| Record<string, unknown>
|
||||||
|
>;
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const optionType of ['global', 'local']) {
|
for (const optionType of ['global', 'local']) {
|
||||||
if (Array.isArray(options[optionType])) {
|
if (Array.isArray(options[optionType])) {
|
||||||
@ -151,12 +163,13 @@ const migrations = {
|
|||||||
export default new Store({
|
export default new Store({
|
||||||
defaults: {
|
defaults: {
|
||||||
...defaults,
|
...defaults,
|
||||||
plugins: Object
|
plugins: Object.entries(allPlugins).reduce(
|
||||||
.entries(pluginBuilders)
|
(prev, [id, plugin]) => ({
|
||||||
.reduce((prev, [id, builder]) => ({
|
|
||||||
...prev,
|
...prev,
|
||||||
[id]: (builder as PluginBuilderList[keyof PluginBuilderList]).config,
|
[id]: plugin.config,
|
||||||
}), {}),
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
clearInvalidConfig: false,
|
clearInvalidConfig: false,
|
||||||
migrations,
|
migrations,
|
||||||
|
|||||||
345
src/index.ts
345
src/index.ts
@ -2,8 +2,19 @@ import path from 'node:path';
|
|||||||
import url from 'node:url';
|
import url from 'node:url';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
|
import {
|
||||||
import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request';
|
BrowserWindow,
|
||||||
|
app,
|
||||||
|
screen,
|
||||||
|
globalShortcut,
|
||||||
|
session,
|
||||||
|
shell,
|
||||||
|
dialog,
|
||||||
|
ipcMain,
|
||||||
|
} from 'electron';
|
||||||
|
import enhanceWebRequest, {
|
||||||
|
BetterSession,
|
||||||
|
} from '@jellybrick/electron-better-web-request';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import unhandled from 'electron-unhandled';
|
import unhandled from 'electron-unhandled';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
@ -12,30 +23,31 @@ import { parse } from 'node-html-parser';
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
import { deepEqual } from 'fast-equals';
|
import { deepEqual } from 'fast-equals';
|
||||||
|
|
||||||
import { mainPlugins } from 'virtual:MainPlugins';
|
import { mainPlugins } from 'virtual:plugins';
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
|
||||||
|
|
||||||
import config from './config';
|
import config from '@/config';
|
||||||
|
|
||||||
import { refreshMenu, setApplicationMenu } from './menu';
|
import { refreshMenu, setApplicationMenu } from '@/menu';
|
||||||
import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils/main';
|
import { fileExists, injectCSS, injectCSSAsFile } from '@/plugins/utils/main';
|
||||||
import { isTesting } from './utils/testing';
|
import { isTesting } from '@/utils/testing';
|
||||||
import { setUpTray } from './tray';
|
import { setUpTray } from '@/tray';
|
||||||
import { setupSongInfo } from './providers/song-info';
|
import { setupSongInfo } from '@/providers/song-info';
|
||||||
import { restart, setupAppControls } from './providers/app-controls';
|
import { restart, setupAppControls } from '@/providers/app-controls';
|
||||||
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
|
import {
|
||||||
|
APP_PROTOCOL,
|
||||||
|
handleProtocol,
|
||||||
|
setupProtocolHandler,
|
||||||
|
} from '@/providers/protocol-handler';
|
||||||
|
|
||||||
|
import youtubeMusicCSS from '@/youtube-music.css?inline';
|
||||||
import youtubeMusicCSS from './youtube-music.css?inline';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
forceLoadMainPlugin,
|
forceLoadMainPlugin,
|
||||||
forceUnloadMainPlugin,
|
forceUnloadMainPlugin,
|
||||||
getAllLoadedMainPlugins,
|
getAllLoadedMainPlugins,
|
||||||
loadAllMainPlugins,
|
loadAllMainPlugins,
|
||||||
registerMainPlugin
|
} from '@/loader/main';
|
||||||
} from './loader/main';
|
import { PluginBaseConfig } from '@/plugins/utils/builder';
|
||||||
import { MainPluginFactory, PluginBaseConfig, PluginBuilder } from './plugins/utils/builder';
|
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -57,7 +69,10 @@ if (!gotTheLock) {
|
|||||||
|
|
||||||
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
|
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
|
||||||
// OverlayScrollbar: Required for overlay scrollbars
|
// OverlayScrollbar: Required for overlay scrollbars
|
||||||
app.commandLine.appendSwitch('enable-features', 'OverlayScrollbar,SharedArrayBuffer');
|
app.commandLine.appendSwitch(
|
||||||
|
'enable-features',
|
||||||
|
'OverlayScrollbar,SharedArrayBuffer',
|
||||||
|
);
|
||||||
if (config.get('options.disableHardwareAcceleration')) {
|
if (config.get('options.disableHardwareAcceleration')) {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log('Disabling hardware acceleration');
|
console.log('Disabling hardware acceleration');
|
||||||
@ -95,42 +110,58 @@ function onClosed() {
|
|||||||
|
|
||||||
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
|
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
|
||||||
|
|
||||||
|
|
||||||
const initHook = (win: BrowserWindow) => {
|
const initHook = (win: BrowserWindow) => {
|
||||||
ipcMain.handle('get-config', (_, id: keyof PluginBuilderList) => deepmerge(pluginBuilders[id].config, config.get(`plugins.${id}`) ?? {}) as PluginBuilderList[typeof id]['config']);
|
ipcMain.handle(
|
||||||
ipcMain.handle('set-config', (_, name: string, obj: object) => config.setPartial(`plugins.${name}`, obj));
|
'get-config',
|
||||||
|
(_, id: keyof PluginBuilderList) =>
|
||||||
|
deepmerge(
|
||||||
|
mainPlugins[id].config,
|
||||||
|
config.get(`plugins.${id}`) ?? {},
|
||||||
|
) as PluginBuilderList[typeof id]['config'],
|
||||||
|
);
|
||||||
|
ipcMain.handle('set-config', (_, name: string, obj: object) =>
|
||||||
|
config.setPartial(`plugins.${name}`, obj),
|
||||||
|
);
|
||||||
|
|
||||||
config.watch((newValue, oldValue) => {
|
config.watch((newValue, oldValue) => {
|
||||||
const newPluginConfigList = (newValue?.plugins ?? {}) as Record<string, unknown>;
|
const newPluginConfigList = (newValue?.plugins ?? {}) as Record<
|
||||||
const oldPluginConfigList = (oldValue?.plugins ?? {}) as Record<string, unknown>;
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
const oldPluginConfigList = (oldValue?.plugins ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
Object.entries(newPluginConfigList).forEach(([id, newPluginConfig]) => {
|
Object.entries(newPluginConfigList).forEach(([id, newPluginConfig]) => {
|
||||||
const isEqual = deepEqual(oldPluginConfigList[id], newPluginConfig);
|
const isEqual = deepEqual(oldPluginConfigList[id], newPluginConfig);
|
||||||
|
|
||||||
if (!isEqual) {
|
if (!isEqual) {
|
||||||
const oldConfig = oldPluginConfigList[id] as PluginBaseConfig;
|
const oldConfig = oldPluginConfigList[id] as PluginBaseConfig;
|
||||||
const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig) as PluginBaseConfig;
|
const config = deepmerge(
|
||||||
|
mainPlugins[id].config,
|
||||||
|
newPluginConfig,
|
||||||
|
) as PluginBaseConfig;
|
||||||
|
|
||||||
if (config.enabled !== oldConfig?.enabled) {
|
if (config.enabled !== oldConfig?.enabled) {
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
win.webContents.send('plugin:enable', id);
|
win.webContents.send('plugin:enable', id);
|
||||||
ipcMain.emit('plugin:enable', id);
|
ipcMain.emit('plugin:enable', id);
|
||||||
forceLoadMainPlugin(id as keyof PluginBuilderList, win);
|
forceLoadMainPlugin(id, win);
|
||||||
} else {
|
} else {
|
||||||
win.webContents.send('plugin:unload', id);
|
win.webContents.send('plugin:unload', id);
|
||||||
ipcMain.emit('plugin:unload', id);
|
ipcMain.emit('plugin:unload', id);
|
||||||
forceUnloadMainPlugin(id as keyof PluginBuilderList, win);
|
forceUnloadMainPlugin(id, win);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginBuilders[id as keyof PluginBuilderList].restartNeeded) {
|
if (mainPlugins[id].restartNeeded) {
|
||||||
showNeedToRestartDialog(id as keyof PluginBuilderList);
|
showNeedToRestartDialog(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainPlugin = getAllLoadedMainPlugins()[id];
|
const mainPlugin = getAllLoadedMainPlugins()[id];
|
||||||
if (mainPlugin) {
|
if (mainPlugin) {
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
mainPlugin.onConfigChange?.(config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,14 +171,15 @@ const initHook = (win: BrowserWindow) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const showNeedToRestartDialog = (id: keyof PluginBuilderList) => {
|
const showNeedToRestartDialog = (id: string) => {
|
||||||
const builder = pluginBuilders[id];
|
const plugin = mainPlugins[id];
|
||||||
|
|
||||||
const dialogOptions: Electron.MessageBoxOptions = {
|
const dialogOptions: Electron.MessageBoxOptions = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['Restart Now', 'Later'],
|
buttons: ['Restart Now', 'Later'],
|
||||||
title: 'Restart Required',
|
title: 'Restart Required',
|
||||||
message: `"${builder.name ?? builder.id}" needs to restart`,
|
message: `"${plugin.name ?? id}" needs to restart`,
|
||||||
detail: `"${builder.name ?? builder.id}" plugin requires a restart to take effect`,
|
detail: `"${plugin.name ?? id}" plugin requires a restart to take effect`,
|
||||||
defaultId: 0,
|
defaultId: 0,
|
||||||
cancelId: 1,
|
cancelId: 1,
|
||||||
};
|
};
|
||||||
@ -186,7 +218,10 @@ function initTheme(win: BrowserWindow) {
|
|||||||
injectCSSAsFile(win.webContents, cssFile);
|
injectCSSAsFile(win.webContents, cssFile);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
console.warn('[YTMusic]', `CSS file "${cssFile}" does not exist, ignoring`);
|
console.warn(
|
||||||
|
'[YTMusic]',
|
||||||
|
`CSS file "${cssFile}" does not exist, ignoring`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,46 +259,43 @@ async function createMainWindow() {
|
|||||||
...(isTesting()
|
...(isTesting()
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
// Sandbox is only enabled in tests for now
|
// Sandbox is only enabled in tests for now
|
||||||
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
frame: !is.macOS() && !useInlineMenu,
|
frame: !is.macOS() && !useInlineMenu,
|
||||||
titleBarOverlay: defaultTitleBarOverlayOptions,
|
titleBarOverlay: defaultTitleBarOverlayOptions,
|
||||||
titleBarStyle: useInlineMenu
|
titleBarStyle: useInlineMenu
|
||||||
? 'hidden'
|
? 'hidden'
|
||||||
: (is.macOS()
|
: is.macOS()
|
||||||
? 'hiddenInset'
|
? 'hiddenInset'
|
||||||
: 'default'),
|
: 'default',
|
||||||
autoHideMenuBar: config.get('options.hideMenu'),
|
autoHideMenuBar: config.get('options.hideMenu'),
|
||||||
});
|
});
|
||||||
initHook(win);
|
initHook(win);
|
||||||
initTheme(win);
|
initTheme(win);
|
||||||
|
|
||||||
Object.entries(pluginBuilders).forEach(([id, builder]) => {
|
|
||||||
const typedBuilder = builder as PluginBuilder<string, PluginBaseConfig>;
|
|
||||||
const plugin = mainPlugins[id] as MainPluginFactory<PluginBaseConfig> | undefined;
|
|
||||||
|
|
||||||
registerMainPlugin(id, typedBuilder, plugin);
|
|
||||||
});
|
|
||||||
await loadAllMainPlugins(win);
|
await loadAllMainPlugins(win);
|
||||||
|
|
||||||
if (windowPosition) {
|
if (windowPosition) {
|
||||||
const { x: windowX, y: windowY } = windowPosition;
|
const { x: windowX, y: windowY } = windowPosition;
|
||||||
const winSize = win.getSize();
|
const winSize = win.getSize();
|
||||||
const displaySize
|
const displaySize = screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||||
= screen.getDisplayNearestPoint(windowPosition).bounds;
|
|
||||||
if (
|
if (
|
||||||
windowX + winSize[0] < displaySize.x - 8
|
windowX + winSize[0] < displaySize.x - 8 ||
|
||||||
|| windowX - winSize[0] > displaySize.x + displaySize.width
|
windowX - winSize[0] > displaySize.x + displaySize.width ||
|
||||||
|| windowY < displaySize.y - 8
|
windowY < displaySize.y - 8 ||
|
||||||
|| windowY > displaySize.y + displaySize.height
|
windowY > displaySize.y + displaySize.height
|
||||||
) {
|
) {
|
||||||
// Window is offscreen
|
// Window is offscreen
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
console.log(
|
||||||
`Window tried to render offscreen, windowSize=${String(winSize)}, displaySize=${String(displaySize)}, position=${String(windowPosition)}`,
|
`Window tried to render offscreen, windowSize=${String(
|
||||||
|
winSize,
|
||||||
|
)}, displaySize=${String(displaySize)}, position=${String(
|
||||||
|
windowPosition,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -316,7 +348,11 @@ async function createMainWindow() {
|
|||||||
|
|
||||||
const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {};
|
const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {};
|
||||||
|
|
||||||
function lateSave(key: string, value: unknown, fn: (key: string, value: unknown) => void = config.set) {
|
function lateSave(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
fn: (key: string, value: unknown) => void = config.set,
|
||||||
|
) {
|
||||||
if (savedTimeouts[key]) {
|
if (savedTimeouts[key]) {
|
||||||
clearTimeout(savedTimeouts[key]);
|
clearTimeout(savedTimeouts[key]);
|
||||||
}
|
}
|
||||||
@ -343,7 +379,10 @@ async function createMainWindow() {
|
|||||||
if (useInlineMenu) {
|
if (useInlineMenu) {
|
||||||
win.setTitleBarOverlay({
|
win.setTitleBarOverlay({
|
||||||
...defaultTitleBarOverlayOptions,
|
...defaultTitleBarOverlayOptions,
|
||||||
height: Math.floor(defaultTitleBarOverlayOptions.height! * win.webContents.getZoomFactor()),
|
height: Math.floor(
|
||||||
|
defaultTitleBarOverlayOptions.height! *
|
||||||
|
win.webContents.getZoomFactor(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,14 +404,25 @@ async function createMainWindow() {
|
|||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
const rendererPath = path.join(__dirname, '..', 'renderer');
|
const rendererPath = path.join(__dirname, '..', 'renderer');
|
||||||
const indexHTML = parse(fs.readFileSync(path.join(rendererPath, 'index.html'), 'utf-8'));
|
const indexHTML = parse(
|
||||||
|
fs.readFileSync(path.join(rendererPath, 'index.html'), 'utf-8'),
|
||||||
|
);
|
||||||
const scriptSrc = indexHTML.querySelector('script')!;
|
const scriptSrc = indexHTML.querySelector('script')!;
|
||||||
const scriptPath = path.join(rendererPath, scriptSrc.getAttribute('src')!);
|
const scriptPath = path.join(
|
||||||
|
rendererPath,
|
||||||
|
scriptSrc.getAttribute('src')!,
|
||||||
|
);
|
||||||
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
|
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
|
||||||
await win.webContents.executeJavaScriptInIsolatedWorld(0, [{
|
await win.webContents.executeJavaScriptInIsolatedWorld(
|
||||||
code: scriptString + ';0',
|
0,
|
||||||
url: url.pathToFileURL(scriptPath).toString(),
|
[
|
||||||
}], true);
|
{
|
||||||
|
code: scriptString + ';0',
|
||||||
|
url: url.pathToFileURL(scriptPath).toString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -387,21 +437,26 @@ app.once('browser-window-created', (event, win) => {
|
|||||||
const originalUserAgent = win.webContents.userAgent;
|
const originalUserAgent = win.webContents.userAgent;
|
||||||
const userAgents = {
|
const userAgents = {
|
||||||
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0',
|
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||||
windows: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
windows:
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||||
linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedUserAgent
|
const updatedUserAgent = is.macOS()
|
||||||
= is.macOS() ? userAgents.mac
|
? userAgents.mac
|
||||||
: (is.windows() ? userAgents.windows
|
: is.windows()
|
||||||
: userAgents.linux);
|
? userAgents.windows
|
||||||
|
: userAgents.linux;
|
||||||
|
|
||||||
win.webContents.userAgent = updatedUserAgent;
|
win.webContents.userAgent = updatedUserAgent;
|
||||||
app.userAgentFallback = updatedUserAgent;
|
app.userAgentFallback = updatedUserAgent;
|
||||||
|
|
||||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||||
// This will only happen if login failed, and "retry" was pressed
|
// This will only happen if login failed, and "retry" was pressed
|
||||||
if (win.webContents.getURL().startsWith('https://accounts.google.com') && details.url.startsWith('https://accounts.google.com')) {
|
if (
|
||||||
|
win.webContents.getURL().startsWith('https://accounts.google.com') &&
|
||||||
|
details.url.startsWith('https://accounts.google.com')
|
||||||
|
) {
|
||||||
details.requestHeaders['User-Agent'] = originalUserAgent;
|
details.requestHeaders['User-Agent'] = originalUserAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,33 +467,41 @@ app.once('browser-window-created', (event, win) => {
|
|||||||
setupSongInfo(win);
|
setupSongInfo(win);
|
||||||
setupAppControls();
|
setupAppControls();
|
||||||
|
|
||||||
win.webContents.on('did-fail-load', (
|
win.webContents.on(
|
||||||
_event,
|
'did-fail-load',
|
||||||
errorCode,
|
(
|
||||||
errorDescription,
|
_event,
|
||||||
validatedURL,
|
|
||||||
isMainFrame,
|
|
||||||
frameProcessId,
|
|
||||||
frameRoutingId,
|
|
||||||
) => {
|
|
||||||
const log = JSON.stringify({
|
|
||||||
error: 'did-fail-load',
|
|
||||||
errorCode,
|
errorCode,
|
||||||
errorDescription,
|
errorDescription,
|
||||||
validatedURL,
|
validatedURL,
|
||||||
isMainFrame,
|
isMainFrame,
|
||||||
frameProcessId,
|
frameProcessId,
|
||||||
frameRoutingId,
|
frameRoutingId,
|
||||||
}, null, '\t');
|
) => {
|
||||||
if (is.dev()) {
|
const log = JSON.stringify(
|
||||||
console.log(log);
|
{
|
||||||
}
|
error: 'did-fail-load',
|
||||||
|
errorCode,
|
||||||
|
errorDescription,
|
||||||
|
validatedURL,
|
||||||
|
isMainFrame,
|
||||||
|
frameProcessId,
|
||||||
|
frameRoutingId,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
'\t',
|
||||||
|
);
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log(log);
|
||||||
|
}
|
||||||
|
|
||||||
if (errorCode !== -3) { // -3 is a false positive
|
if (errorCode !== -3) {
|
||||||
win.webContents.send('log', log);
|
// -3 is a false positive
|
||||||
win.webContents.loadFile(path.join(__dirname, 'error.html'));
|
win.webContents.send('log', log);
|
||||||
}
|
win.webContents.loadFile(path.join(__dirname, 'error.html'));
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
win.webContents.on('will-prevent-unload', (event) => {
|
win.webContents.on('will-prevent-unload', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -484,17 +547,29 @@ app.on('ready', async () => {
|
|||||||
const appLocation = process.execPath;
|
const appLocation = process.execPath;
|
||||||
const appData = app.getPath('appData');
|
const appData = app.getPath('appData');
|
||||||
// Check shortcut validity if not in dev mode / running portable app
|
// Check shortcut validity if not in dev mode / running portable app
|
||||||
if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
|
if (
|
||||||
const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
|
!is.dev() &&
|
||||||
try { // Check if shortcut is registered and valid
|
!appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))
|
||||||
|
) {
|
||||||
|
const shortcutPath = path.join(
|
||||||
|
appData,
|
||||||
|
'Microsoft',
|
||||||
|
'Windows',
|
||||||
|
'Start Menu',
|
||||||
|
'Programs',
|
||||||
|
'YouTube Music.lnk',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// Check if shortcut is registered and valid
|
||||||
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
||||||
if (
|
if (
|
||||||
shortcutDetails.target !== appLocation
|
shortcutDetails.target !== appLocation ||
|
||||||
|| shortcutDetails.appUserModelId !== appID
|
shortcutDetails.appUserModelId !== appID
|
||||||
) {
|
) {
|
||||||
throw 'needUpdate';
|
throw 'needUpdate';
|
||||||
}
|
}
|
||||||
} catch (error) { // If not valid -> Register shortcut
|
} catch (error) {
|
||||||
|
// If not valid -> Register shortcut
|
||||||
shell.writeShortcutLink(
|
shell.writeShortcutLink(
|
||||||
shortcutPath,
|
shortcutPath,
|
||||||
error === 'needUpdate' ? 'update' : 'create',
|
error === 'needUpdate' ? 'update' : 'create',
|
||||||
@ -556,8 +631,8 @@ app.on('ready', async () => {
|
|||||||
clearTimeout(updateTimeout);
|
clearTimeout(updateTimeout);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
autoUpdater.on('update-available', () => {
|
autoUpdater.on('update-available', () => {
|
||||||
const downloadLink
|
const downloadLink =
|
||||||
= 'https://github.com/th-ch/youtube-music/releases/latest';
|
'https://github.com/th-ch/youtube-music/releases/latest';
|
||||||
const dialogOptions: Electron.MessageBoxOptions = {
|
const dialogOptions: Electron.MessageBoxOptions = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK', 'Download', 'Disable updates'],
|
buttons: ['OK', 'Download', 'Disable updates'],
|
||||||
@ -597,8 +672,10 @@ app.on('ready', async () => {
|
|||||||
|
|
||||||
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
type: 'info', title: 'Hide Menu Enabled',
|
type: 'info',
|
||||||
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
|
title: 'Hide Menu Enabled',
|
||||||
|
message:
|
||||||
|
"Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
|
||||||
});
|
});
|
||||||
config.set('options.hideMenuWarned', true);
|
config.set('options.hideMenuWarned', true);
|
||||||
}
|
}
|
||||||
@ -624,31 +701,36 @@ app.on('ready', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProcessGoneDetails) {
|
function showUnresponsiveDialog(
|
||||||
|
win: BrowserWindow,
|
||||||
|
details: Electron.RenderProcessGoneDetails,
|
||||||
|
) {
|
||||||
if (details) {
|
if (details) {
|
||||||
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showMessageBox(win, {
|
dialog
|
||||||
type: 'error',
|
.showMessageBox(win, {
|
||||||
title: 'Window Unresponsive',
|
type: 'error',
|
||||||
message: 'The Application is Unresponsive',
|
title: 'Window Unresponsive',
|
||||||
detail: 'We are sorry for the inconvenience! please choose what to do:',
|
message: 'The Application is Unresponsive',
|
||||||
buttons: ['Wait', 'Relaunch', 'Quit'],
|
detail: 'We are sorry for the inconvenience! please choose what to do:',
|
||||||
cancelId: 0,
|
buttons: ['Wait', 'Relaunch', 'Quit'],
|
||||||
}).then((result) => {
|
cancelId: 0,
|
||||||
switch (result.response) {
|
})
|
||||||
case 1: {
|
.then((result) => {
|
||||||
restart();
|
switch (result.response) {
|
||||||
break;
|
case 1: {
|
||||||
}
|
restart();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 2: {
|
case 2: {
|
||||||
app.quit();
|
app.quit();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeContentSecurityPolicy(
|
function removeContentSecurityPolicy(
|
||||||
@ -671,18 +753,21 @@ function removeContentSecurityPolicy(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// When multiple listeners are defined, apply them all
|
// When multiple listeners are defined, apply them all
|
||||||
betterSession.webRequest.setResolver('onHeadersReceived', async (listeners) => {
|
betterSession.webRequest.setResolver(
|
||||||
return listeners.reduce(
|
'onHeadersReceived',
|
||||||
async (accumulator, listener) => {
|
async (listeners) => {
|
||||||
const acc = await accumulator;
|
return listeners.reduce(
|
||||||
if (acc.cancel) {
|
async (accumulator, listener) => {
|
||||||
return acc;
|
const acc = await accumulator;
|
||||||
}
|
if (acc.cancel) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await listener.apply();
|
const result = await listener.apply();
|
||||||
return { ...accumulator, ...result };
|
return { ...accumulator, ...result };
|
||||||
},
|
},
|
||||||
Promise.resolve({ cancel: false }),
|
Promise.resolve({ cancel: false }),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,24 @@
|
|||||||
|
/* 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 config from '../config';
|
import { PluginDef } from '@/types/plugins';
|
||||||
import { injectCSS } from '../plugins/utils/main';
|
import { BackendContext } from '@/types/contexts';
|
||||||
import {
|
import config from '@/config';
|
||||||
MainPlugin,
|
import { startPlugin, stopPlugin } from '@/utils';
|
||||||
MainPluginContext,
|
|
||||||
MainPluginFactory,
|
|
||||||
PluginBaseConfig,
|
|
||||||
PluginBuilder
|
|
||||||
} from '../plugins/utils/builder';
|
|
||||||
|
|
||||||
const allPluginFactoryList: Record<string, MainPluginFactory<PluginBaseConfig>> = {};
|
const loadedPluginMap: Record<string, PluginDef> = {};
|
||||||
const allPluginBuilders: Record<string, PluginBuilder<string, PluginBaseConfig>> = {};
|
|
||||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
|
||||||
const loadedPluginMap: Record<string, MainPlugin<PluginBaseConfig>> = {};
|
|
||||||
|
|
||||||
const createContext = <
|
const createContext = (id: string, win: BrowserWindow): BackendContext => ({
|
||||||
Key extends keyof PluginBuilderList,
|
getConfig: () =>
|
||||||
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
// @ts-expect-error ts dum dum
|
||||||
>(id: Key, win: BrowserWindow): MainPluginContext<Config> => ({
|
deepmerge(
|
||||||
getConfig: () => deepmerge(allPluginBuilders[id].config, config.get(`plugins.${id}`) ?? {}) as Config,
|
mainPlugins[id].config,
|
||||||
|
config.get(`plugins.${id}`) ?? { enabled: false },
|
||||||
|
),
|
||||||
setConfig: (newConfig) => {
|
setConfig: (newConfig) => {
|
||||||
config.setPartial(`plugins.${id}`, newConfig);
|
config.setPartial(`plugins.${id}`, newConfig);
|
||||||
},
|
},
|
||||||
@ -29,81 +26,95 @@ const createContext = <
|
|||||||
send: (event: string, ...args: unknown[]) => {
|
send: (event: string, ...args: unknown[]) => {
|
||||||
win.webContents.send(event, ...args);
|
win.webContents.send(event, ...args);
|
||||||
},
|
},
|
||||||
|
// @ts-expect-error ts dum dum
|
||||||
handle: (event: string, listener) => {
|
handle: (event: string, listener) => {
|
||||||
ipcMain.handle(event, async (_, ...args) => listener(...args as never));
|
// @ts-expect-error ts dum dum
|
||||||
|
ipcMain.handle(event, (_, ...args) => listener(...(args as never)));
|
||||||
},
|
},
|
||||||
|
// @ts-expect-error ts dum dum
|
||||||
on: (event: string, listener) => {
|
on: (event: string, listener) => {
|
||||||
ipcMain.on(event, async (_, ...args) => listener(...args as never));
|
// @ts-expect-error ts dum dum
|
||||||
|
ipcMain.on(event, (_, ...args) => listener(...(args as never)));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forceUnloadMainPlugin = (id: keyof PluginBuilderList, win: BrowserWindow) => {
|
export const forceUnloadMainPlugin = async (
|
||||||
unregisterStyleMap[id]?.forEach((unregister) => unregister());
|
id: string,
|
||||||
delete unregisterStyleMap[id];
|
win: BrowserWindow,
|
||||||
|
): Promise<void> => {
|
||||||
|
const plugin = loadedPluginMap[id]!;
|
||||||
|
if (!plugin) return;
|
||||||
|
|
||||||
loadedPluginMap[id]?.onUnload?.(win);
|
return new Promise<void>((resolve, reject) => {
|
||||||
delete loadedPluginMap[id];
|
try {
|
||||||
|
const hasStopped = stopPlugin(id, plugin, {
|
||||||
|
ctx: 'backend',
|
||||||
|
context: createContext(id, win),
|
||||||
|
});
|
||||||
|
if (!hasStopped) {
|
||||||
|
console.log(
|
||||||
|
'[YTMusic]',
|
||||||
|
`Cannot unload "${id}" plugin: no stop function`,
|
||||||
|
);
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
delete loadedPluginMap[id];
|
||||||
|
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[YTMusic]', `Cannot unload "${id}" plugin: ${String(err)}`);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const forceLoadMainPlugin = async (id: keyof PluginBuilderList, win: BrowserWindow) => {
|
export const forceLoadMainPlugin = async (
|
||||||
const builder = allPluginBuilders[id];
|
id: string,
|
||||||
|
win: BrowserWindow,
|
||||||
|
): Promise<void> => {
|
||||||
|
const plugin = mainPlugins[id];
|
||||||
|
if (!plugin.backend) return;
|
||||||
|
|
||||||
Promise.allSettled(
|
return new Promise<void>((resolve, reject) => {
|
||||||
builder.styles?.map(async (style) => {
|
try {
|
||||||
const unregister = await injectCSS(win.webContents, style);
|
const hasStarted = startPlugin(id, plugin, {
|
||||||
console.log('[YTMusic]', `Injected CSS for "${id}" plugin`);
|
ctx: 'backend',
|
||||||
|
context: createContext(id, win),
|
||||||
return unregister;
|
});
|
||||||
}) ?? [],
|
if (!hasStarted) {
|
||||||
).then((result) => {
|
console.log('[YTMusic]', `Cannot load "${id}" plugin`);
|
||||||
unregisterStyleMap[id] = result
|
reject();
|
||||||
.map((it) => it.status === 'fulfilled' && it.value)
|
return;
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
let isInjectSuccess = true;
|
|
||||||
result.forEach((it) => {
|
|
||||||
if (it.status === 'rejected') {
|
|
||||||
isInjectSuccess = false;
|
|
||||||
|
|
||||||
console.log('[YTMusic]', `Cannot inject "${id}" plugin style: ${String(it.reason)}`);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
if (isInjectSuccess) console.log('[YTMusic]', `"${id}" plugin data is loaded`);
|
loadedPluginMap[id] = plugin;
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
'[YTMusic]',
|
||||||
|
`Cannot initialize "${id}" plugin: ${String(err)}`,
|
||||||
|
);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
const factory = allPluginFactoryList[id];
|
|
||||||
if (!factory) return;
|
|
||||||
|
|
||||||
const context = createContext(id, win);
|
|
||||||
const plugin = await factory(context);
|
|
||||||
loadedPluginMap[id] = plugin;
|
|
||||||
plugin.onLoad?.(win);
|
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('[YTMusic]', `Cannot initialize "${id}" plugin: ${String(err)}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAllMainPlugins = async (win: BrowserWindow) => {
|
export const loadAllMainPlugins = async (win: BrowserWindow) => {
|
||||||
const pluginConfigs = config.plugins.getPlugins();
|
const pluginConfigs = config.plugins.getPlugins();
|
||||||
|
const queue: Promise<void>[] = [];
|
||||||
|
|
||||||
for (const [pluginId, builder] of Object.entries(allPluginBuilders)) {
|
for (const [plugin, pluginDef] of Object.entries(mainPlugins)) {
|
||||||
const typedBuilder = builder as PluginBuilderList[keyof PluginBuilderList];
|
const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {});
|
||||||
|
|
||||||
const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {});
|
|
||||||
|
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
await forceLoadMainPlugin(pluginId as keyof PluginBuilderList, win);
|
queue.push(forceLoadMainPlugin(plugin, win));
|
||||||
} else {
|
} else if (loadedPluginMap[plugin]) {
|
||||||
if (loadedPluginMap[pluginId as keyof PluginBuilderList]) {
|
queue.push(forceUnloadMainPlugin(plugin, win));
|
||||||
forceUnloadMainPlugin(pluginId as keyof PluginBuilderList, win);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(queue);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unloadAllMainPlugins = (win: BrowserWindow) => {
|
export const unloadAllMainPlugins = (win: BrowserWindow) => {
|
||||||
@ -112,17 +123,10 @@ export const unloadAllMainPlugins = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLoadedMainPlugin = <Key extends keyof PluginBuilderList>(id: Key): MainPlugin<PluginBuilderList[Key]['config']> | undefined => {
|
export const getLoadedMainPlugin = (id: string): PluginDef | undefined => {
|
||||||
return loadedPluginMap[id];
|
return loadedPluginMap[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoadedMainPlugins = () => {
|
export const getAllLoadedMainPlugins = () => {
|
||||||
return loadedPluginMap;
|
return loadedPluginMap;
|
||||||
};
|
};
|
||||||
export const registerMainPlugin = (
|
|
||||||
id: string,
|
|
||||||
builder: PluginBuilder<string, PluginBaseConfig>,
|
|
||||||
factory?: MainPluginFactory<PluginBaseConfig>,
|
|
||||||
) => {
|
|
||||||
if (factory) allPluginFactoryList[id] = factory;
|
|
||||||
allPluginBuilders[id] = builder;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,26 +1,21 @@
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
|
import { allPlugins } from 'virtual:plugins';
|
||||||
|
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
import { MenuPluginContext, MenuPluginFactory, PluginBaseConfig, PluginBuilder } from '../plugins/utils/builder';
|
import { MenuContext } from '@/types/contexts';
|
||||||
import config from '../config';
|
|
||||||
import { setApplicationMenu } from '../menu';
|
|
||||||
|
|
||||||
import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
import config from '@/config';
|
||||||
|
import { setApplicationMenu } from '@/menu';
|
||||||
|
|
||||||
const allPluginFactoryList: Record<string, MenuPluginFactory<PluginBaseConfig>> = {};
|
|
||||||
const allPluginBuilders: Record<string, PluginBuilder<string, PluginBaseConfig>> = {};
|
|
||||||
const menuTemplateMap: Record<string, MenuItemConstructorOptions[]> = {};
|
const menuTemplateMap: Record<string, MenuItemConstructorOptions[]> = {};
|
||||||
|
const createContext = (id: string, win: BrowserWindow): MenuContext => ({
|
||||||
const createContext = <
|
getConfig: () => config.plugins.getOptions(id),
|
||||||
Key extends keyof PluginBuilderList,
|
|
||||||
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
|
||||||
>(id: Key, win: BrowserWindow): MenuPluginContext<Config> => ({
|
|
||||||
getConfig: () => deepmerge(allPluginBuilders[id].config, config.get(`plugins.${id}`) ?? {}) as Config,
|
|
||||||
setConfig: (newConfig) => {
|
setConfig: (newConfig) => {
|
||||||
config.setPartial(`plugins.${id}`, newConfig);
|
config.setPartial(`plugins.${id}`, newConfig);
|
||||||
},
|
},
|
||||||
window: win,
|
window: win,
|
||||||
refresh: async () => {
|
refresh: () => {
|
||||||
await setApplicationMenu(win);
|
setApplicationMenu(win);
|
||||||
|
|
||||||
if (config.plugins.isEnabled('in-app-menu')) {
|
if (config.plugins.isEnabled('in-app-menu')) {
|
||||||
win.webContents.send('refresh-in-app-menu');
|
win.webContents.send('refresh-in-app-menu');
|
||||||
@ -28,45 +23,39 @@ const createContext = <
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forceLoadMenuPlugin = async (id: keyof PluginBuilderList, win: BrowserWindow) => {
|
export const forceLoadMenuPlugin = (id: string, win: BrowserWindow) => {
|
||||||
try {
|
try {
|
||||||
const factory = allPluginFactoryList[id];
|
const plugin = allPlugins[id];
|
||||||
if (!factory) return;
|
if (!plugin) return;
|
||||||
|
|
||||||
const context = createContext(id, win);
|
const menu = plugin.menu?.(createContext(id, win));
|
||||||
menuTemplateMap[id] = await factory(context);
|
if (menu) menuTemplateMap[id] = menu;
|
||||||
|
else return;
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
console.log('[YTMusic]', `Successfully loaded '${id}::menu'`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('[YTMusic]', `Cannot initialize "${id}" plugin: ${String(err)}`);
|
console.log('[YTMusic]', `Cannot initialize '${id}::menu': ${String(err)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAllMenuPlugins = async (win: BrowserWindow) => {
|
export const loadAllMenuPlugins = (win: BrowserWindow) => {
|
||||||
const pluginConfigs = config.plugins.getPlugins();
|
const pluginConfigs = config.plugins.getPlugins();
|
||||||
|
|
||||||
for (const [pluginId, builder] of Object.entries(allPluginBuilders)) {
|
for (const [pluginId, pluginDef] of Object.entries(allPlugins)) {
|
||||||
const typedBuilder = builder as PluginBuilderList[keyof PluginBuilderList];
|
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
|
||||||
|
|
||||||
const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {});
|
|
||||||
|
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
await forceLoadMenuPlugin(pluginId as keyof PluginBuilderList, win);
|
forceLoadMenuPlugin(pluginId, win);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMenuTemplate = <Key extends keyof PluginBuilderList>(id: Key): MenuItemConstructorOptions[] | undefined => {
|
export const getMenuTemplate = (
|
||||||
|
id: string,
|
||||||
|
): MenuItemConstructorOptions[] | undefined => {
|
||||||
return menuTemplateMap[id];
|
return menuTemplateMap[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllMenuTemplate = () => {
|
export const getAllMenuTemplate = () => {
|
||||||
return menuTemplateMap;
|
return menuTemplateMap;
|
||||||
};
|
};
|
||||||
export const registerMenuPlugin = (
|
|
||||||
id: string,
|
|
||||||
builder: PluginBuilder<string, PluginBaseConfig>,
|
|
||||||
factory?: MenuPluginFactory<PluginBaseConfig>,
|
|
||||||
) => {
|
|
||||||
if (factory) allPluginFactoryList[id] = factory;
|
|
||||||
allPluginBuilders[id] = builder;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,68 +1,64 @@
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
|
import { preloadPlugins } from 'virtual:plugins';
|
||||||
|
|
||||||
import {
|
import { type PluginDef } from '@/types/plugins';
|
||||||
PluginBaseConfig,
|
import { type PreloadContext } from '@/types/contexts';
|
||||||
PluginBuilder,
|
import { startPlugin, stopPlugin } from '@/utils';
|
||||||
PreloadPlugin,
|
|
||||||
PluginContext,
|
|
||||||
PreloadPluginFactory
|
|
||||||
} from '../plugins/utils/builder';
|
|
||||||
import config from '../config';
|
|
||||||
|
|
||||||
const allPluginFactoryList: Record<string, PreloadPluginFactory<PluginBaseConfig>> = {};
|
import config from '@/config';
|
||||||
const allPluginBuilders: Record<string, PluginBuilder<string, PluginBaseConfig>> = {};
|
|
||||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
|
||||||
const loadedPluginMap: Record<string, PreloadPlugin<PluginBaseConfig>> = {};
|
|
||||||
|
|
||||||
const createContext = <
|
const loadedPluginMap: Record<string, PluginDef> = {};
|
||||||
Key extends keyof PluginBuilderList,
|
const createContext = (id: string): PreloadContext => ({
|
||||||
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
getConfig: () => config.plugins.getOptions(id),
|
||||||
>(id: Key): PluginContext<Config> => ({
|
|
||||||
getConfig: () => deepmerge(allPluginBuilders[id].config, config.get(`plugins.${id}`) ?? {}) as Config,
|
|
||||||
setConfig: (newConfig) => {
|
setConfig: (newConfig) => {
|
||||||
config.setPartial(`plugins.${id}`, newConfig);
|
config.setPartial(`plugins.${id}`, newConfig);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forceUnloadPreloadPlugin = (id: keyof PluginBuilderList) => {
|
export const forceUnloadPreloadPlugin = (id: string) => {
|
||||||
unregisterStyleMap[id]?.forEach((unregister) => unregister());
|
const hasStopped = stopPlugin(id, loadedPluginMap[id], {
|
||||||
delete unregisterStyleMap[id];
|
ctx: 'preload',
|
||||||
|
context: createContext(id),
|
||||||
loadedPluginMap[id]?.onUnload?.();
|
});
|
||||||
delete loadedPluginMap[id];
|
if (!hasStopped) {
|
||||||
|
console.log('[YTMusic]', `Cannot stop "${id}" plugin`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const forceLoadPreloadPlugin = async (id: keyof PluginBuilderList) => {
|
export const forceLoadPreloadPlugin = (id: string) => {
|
||||||
try {
|
try {
|
||||||
const factory = allPluginFactoryList[id];
|
const plugin = preloadPlugins[id];
|
||||||
if (!factory) return;
|
if (!plugin) return;
|
||||||
|
|
||||||
const context = createContext(id);
|
const hasStarted = startPlugin(id, plugin, {
|
||||||
const plugin = await factory(context);
|
ctx: 'preload',
|
||||||
loadedPluginMap[id] = plugin;
|
context: createContext(id),
|
||||||
plugin.onLoad?.();
|
});
|
||||||
|
|
||||||
|
if (hasStarted) loadedPluginMap[id] = plugin;
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('[YTMusic]', `Cannot initialize "${id}" plugin: ${String(err)}`);
|
console.log(
|
||||||
|
'[YTMusic]',
|
||||||
|
`Cannot initialize "${id}" plugin: ${String(err)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAllPreloadPlugins = async () => {
|
export const loadAllPreloadPlugins = () => {
|
||||||
const pluginConfigs = config.plugins.getPlugins();
|
const pluginConfigs = config.plugins.getPlugins();
|
||||||
|
|
||||||
for (const [pluginId, builder] of Object.entries(allPluginBuilders)) {
|
for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) {
|
||||||
const typedBuilder = builder as PluginBuilderList[keyof PluginBuilderList];
|
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
|
||||||
|
|
||||||
const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {});
|
|
||||||
|
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
await forceLoadPreloadPlugin(pluginId as keyof PluginBuilderList);
|
forceLoadPreloadPlugin(pluginId);
|
||||||
} else {
|
} else {
|
||||||
if (loadedPluginMap[pluginId as keyof PluginBuilderList]) {
|
if (loadedPluginMap[pluginId]) {
|
||||||
forceUnloadPreloadPlugin(pluginId as keyof PluginBuilderList);
|
forceUnloadPreloadPlugin(pluginId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,17 +70,10 @@ export const unloadAllPreloadPlugins = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLoadedPreloadPlugin = <Key extends keyof PluginBuilderList>(id: Key): PreloadPlugin<PluginBuilderList[Key]['config']> | undefined => {
|
export const getLoadedPreloadPlugin = (id: string): PluginDef | undefined => {
|
||||||
return loadedPluginMap[id];
|
return loadedPluginMap[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoadedPreloadPlugins = () => {
|
export const getAllLoadedPreloadPlugins = () => {
|
||||||
return loadedPluginMap;
|
return loadedPluginMap;
|
||||||
};
|
};
|
||||||
export const registerPreloadPlugin = (
|
|
||||||
id: string,
|
|
||||||
builder: PluginBuilder<string, PluginBaseConfig>,
|
|
||||||
factory?: PreloadPluginFactory<PluginBaseConfig>,
|
|
||||||
) => {
|
|
||||||
if (factory) allPluginFactoryList[id] = factory;
|
|
||||||
allPluginBuilders[id] = builder;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,75 +1,75 @@
|
|||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
|
|
||||||
import {
|
import { rendererPlugins } from 'virtual:plugins';
|
||||||
PluginBaseConfig, PluginBuilder,
|
|
||||||
RendererPlugin,
|
import { RendererContext } from '@/types/contexts';
|
||||||
RendererPluginContext,
|
|
||||||
RendererPluginFactory
|
import { PluginDef } from '@/types/plugins';
|
||||||
} from '../plugins/utils/builder';
|
import { startPlugin, stopPlugin } from '@/utils';
|
||||||
|
|
||||||
const allPluginFactoryList: Record<string, RendererPluginFactory<PluginBaseConfig>> = {};
|
|
||||||
const allPluginBuilders: Record<string, PluginBuilder<string, PluginBaseConfig>> = {};
|
|
||||||
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
const unregisterStyleMap: Record<string, (() => void)[]> = {};
|
||||||
const loadedPluginMap: Record<string, RendererPlugin<PluginBaseConfig>> = {};
|
const loadedPluginMap: Record<string, PluginDef> = {};
|
||||||
|
|
||||||
const createContext = <
|
const createContext = (id: string): RendererContext => ({
|
||||||
Key extends keyof PluginBuilderList,
|
getConfig: () => window.mainConfig.plugins.getOptions(id),
|
||||||
Config extends PluginBaseConfig = PluginBuilderList[Key]['config'],
|
|
||||||
>(id: Key): RendererPluginContext<Config> => ({
|
|
||||||
getConfig: async () => {
|
|
||||||
return await window.ipcRenderer.invoke('get-config', id) as Config;
|
|
||||||
},
|
|
||||||
setConfig: async (newConfig) => {
|
setConfig: async (newConfig) => {
|
||||||
await window.ipcRenderer.invoke('set-config', id, newConfig);
|
await window.ipcRenderer.invoke('set-config', id, newConfig);
|
||||||
},
|
},
|
||||||
|
|
||||||
invoke: async <Return>(event: string, ...args: unknown[]): Promise<Return> => {
|
|
||||||
return await window.ipcRenderer.invoke(event, ...args) as Return;
|
|
||||||
},
|
|
||||||
on: (event: string, listener) => {
|
|
||||||
window.ipcRenderer.on(event, async (_, ...args) => listener(...args as never));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forceUnloadRendererPlugin = (id: keyof PluginBuilderList) => {
|
export const forceUnloadRendererPlugin = (id: string) => {
|
||||||
unregisterStyleMap[id]?.forEach((unregister) => unregister());
|
unregisterStyleMap[id]?.forEach((unregister) => unregister());
|
||||||
delete unregisterStyleMap[id];
|
|
||||||
|
|
||||||
loadedPluginMap[id]?.onUnload?.();
|
delete unregisterStyleMap[id];
|
||||||
delete loadedPluginMap[id];
|
delete loadedPluginMap[id];
|
||||||
|
|
||||||
|
const plugin = rendererPlugins[id];
|
||||||
|
if (!plugin) return;
|
||||||
|
|
||||||
|
stopPlugin(id, plugin, { ctx: 'renderer', context: createContext(id) });
|
||||||
|
if (plugin.renderer?.stylesheet)
|
||||||
|
document.querySelector(`style#plugin-${id}`)?.remove();
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
console.log('[YTMusic]', `"${id}" plugin is unloaded`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const forceLoadRendererPlugin = async (id: keyof PluginBuilderList) => {
|
export const forceLoadRendererPlugin = (id: string) => {
|
||||||
try {
|
const plugin = rendererPlugins[id];
|
||||||
const factory = allPluginFactoryList[id];
|
if (!plugin) return;
|
||||||
if (!factory) return;
|
|
||||||
|
|
||||||
const context = createContext(id);
|
const hasEvaled = startPlugin(id, plugin, {
|
||||||
const plugin = await factory(context);
|
ctx: 'renderer',
|
||||||
|
context: createContext(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEvaled || plugin.renderer?.stylesheet) {
|
||||||
loadedPluginMap[id] = plugin;
|
loadedPluginMap[id] = plugin;
|
||||||
plugin.onLoad?.();
|
|
||||||
|
|
||||||
console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
if (plugin.renderer?.stylesheet)
|
||||||
} catch (err) {
|
document.head.appendChild(
|
||||||
console.log('[YTMusic]', `Cannot initialize "${id}" plugin: ${String(err)}`);
|
Object.assign(document.createElement('style'), {
|
||||||
|
id: `plugin-${id}`,
|
||||||
|
innerHTML: plugin.renderer?.stylesheet ?? '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasEvaled) console.log('[YTMusic]', `"${id}" plugin is loaded`);
|
||||||
|
} else {
|
||||||
|
console.log('[YTMusic]', `Cannot initialize "${id}" plugin`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAllRendererPlugins = async () => {
|
export const loadAllRendererPlugins = () => {
|
||||||
const pluginConfigs = window.mainConfig.plugins.getPlugins();
|
const pluginConfigs = window.mainConfig.plugins.getPlugins();
|
||||||
|
|
||||||
for (const [pluginId, builder] of Object.entries(allPluginBuilders)) {
|
for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) {
|
||||||
const typedBuilder = builder as PluginBuilderList[keyof PluginBuilderList];
|
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
|
||||||
|
|
||||||
const config = deepmerge(typedBuilder.config, pluginConfigs[pluginId as keyof PluginBuilderList] ?? {});
|
|
||||||
|
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
await forceLoadRendererPlugin(pluginId as keyof PluginBuilderList);
|
forceLoadRendererPlugin(pluginId);
|
||||||
} else {
|
} else {
|
||||||
if (loadedPluginMap[pluginId as keyof PluginBuilderList]) {
|
if (loadedPluginMap[pluginId]) {
|
||||||
forceUnloadRendererPlugin(pluginId as keyof PluginBuilderList);
|
forceUnloadRendererPlugin(pluginId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,21 +77,14 @@ export const loadAllRendererPlugins = async () => {
|
|||||||
|
|
||||||
export const unloadAllRendererPlugins = () => {
|
export const unloadAllRendererPlugins = () => {
|
||||||
for (const id of Object.keys(loadedPluginMap)) {
|
for (const id of Object.keys(loadedPluginMap)) {
|
||||||
forceUnloadRendererPlugin(id as keyof PluginBuilderList);
|
forceUnloadRendererPlugin(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLoadedRendererPlugin = <Key extends keyof PluginBuilderList>(id: Key): RendererPlugin<PluginBuilderList[Key]['config']> | undefined => {
|
export const getLoadedRendererPlugin = (id: string): PluginDef | undefined => {
|
||||||
return loadedPluginMap[id];
|
return loadedPluginMap[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoadedRendererPlugins = () => {
|
export const getAllLoadedRendererPlugins = () => {
|
||||||
return loadedPluginMap;
|
return loadedPluginMap;
|
||||||
};
|
};
|
||||||
export const registerRendererPlugin = (
|
|
||||||
id: string,
|
|
||||||
builder: PluginBuilder<string, PluginBaseConfig>,
|
|
||||||
factory?: RendererPluginFactory<PluginBaseConfig>,
|
|
||||||
) => {
|
|
||||||
if (factory) allPluginFactoryList[id] = factory;
|
|
||||||
allPluginBuilders[id] = builder;
|
|
||||||
};
|
|
||||||
|
|||||||
273
src/menu.ts
273
src/menu.ts
@ -1,5 +1,12 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
import {
|
||||||
|
app,
|
||||||
|
BrowserWindow,
|
||||||
|
clipboard,
|
||||||
|
dialog,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
} from 'electron';
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { restart } from './providers/app-controls';
|
import { restart } from './providers/app-controls';
|
||||||
@ -8,24 +15,22 @@ import { startingPages } from './providers/extracted-data';
|
|||||||
import promptOptions from './providers/prompt-options';
|
import promptOptions from './providers/prompt-options';
|
||||||
|
|
||||||
/* eslint-disable import/order */
|
/* eslint-disable import/order */
|
||||||
import { menuPlugins as menuList } from 'virtual:MenuPlugins';
|
import { allPlugins } from 'virtual:plugins';
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
|
||||||
/* eslint-enable import/order */
|
/* eslint-enable import/order */
|
||||||
|
|
||||||
import { getAvailablePluginNames } from './plugins/utils/main';
|
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
|
||||||
import {
|
|
||||||
MenuPluginFactory,
|
|
||||||
PluginBaseConfig,
|
|
||||||
PluginBuilder
|
|
||||||
} from './plugins/utils/builder';
|
|
||||||
import { getAllMenuTemplate, loadAllMenuPlugins, registerMenuPlugin } from './loader/menu';
|
|
||||||
|
|
||||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||||
|
|
||||||
// True only if in-app-menu was loaded on launch
|
// True only if in-app-menu was loaded on launch
|
||||||
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
||||||
|
|
||||||
const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refreshMenu: (() => void ) | undefined = undefined): Electron.MenuItemConstructorOptions => ({
|
const pluginEnabledMenu = (
|
||||||
|
plugin: string,
|
||||||
|
label = '',
|
||||||
|
hasSubmenu = false,
|
||||||
|
refreshMenu: (() => void) | undefined = undefined,
|
||||||
|
): Electron.MenuItemConstructorOptions => ({
|
||||||
label: label || plugin,
|
label: label || plugin,
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.plugins.isEnabled(plugin),
|
checked: config.plugins.isEnabled(plugin),
|
||||||
@ -49,47 +54,42 @@ export const refreshMenu = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(pluginBuilders).forEach(([id, builder]) => {
|
export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||||
const typedBuilder = builder as PluginBuilder<string, PluginBaseConfig>;
|
|
||||||
const plugin = menuList[id] as MenuPluginFactory<PluginBaseConfig> | undefined;
|
|
||||||
|
|
||||||
registerMenuPlugin(id, typedBuilder, plugin);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate> => {
|
|
||||||
const innerRefreshMenu = () => refreshMenu(win);
|
const innerRefreshMenu = () => refreshMenu(win);
|
||||||
|
|
||||||
await loadAllMenuPlugins(win);
|
loadAllMenuPlugins(win);
|
||||||
|
|
||||||
const menuResult = Object.entries(getAllMenuTemplate()).map(([id, template]) => {
|
const menuResult = Object.entries(getAllMenuTemplate()).map(
|
||||||
const pluginLabel = (pluginBuilders[id as keyof PluginBuilderList])?.name ?? id;
|
([id, template]) => {
|
||||||
|
const pluginLabel = allPlugins[id]?.name ?? id;
|
||||||
|
|
||||||
|
if (!config.plugins.isEnabled(id)) {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu),
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.plugins.isEnabled(id)) {
|
|
||||||
return [
|
return [
|
||||||
id,
|
id,
|
||||||
pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu),
|
{
|
||||||
|
label: pluginLabel,
|
||||||
|
submenu: [
|
||||||
|
pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu),
|
||||||
|
{ type: 'separator' },
|
||||||
|
...template,
|
||||||
|
],
|
||||||
|
} satisfies Electron.MenuItemConstructorOptions,
|
||||||
] as const;
|
] as const;
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
const availablePlugins = Object.keys(allPlugins);
|
||||||
id,
|
|
||||||
{
|
|
||||||
label: pluginLabel,
|
|
||||||
submenu: [
|
|
||||||
pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu),
|
|
||||||
{ type: 'separator' },
|
|
||||||
...template,
|
|
||||||
],
|
|
||||||
} satisfies Electron.MenuItemConstructorOptions
|
|
||||||
] as const;
|
|
||||||
});
|
|
||||||
|
|
||||||
const availablePlugins = getAvailablePluginNames();
|
|
||||||
const pluginMenus = availablePlugins.map((id) => {
|
const pluginMenus = availablePlugins.map((id) => {
|
||||||
const predefinedTemplate = menuResult.find((it) => it[0] === id);
|
const predefinedTemplate = menuResult.find((it) => it[0] === id);
|
||||||
if (predefinedTemplate) return predefinedTemplate[1];
|
if (predefinedTemplate) return predefinedTemplate[1];
|
||||||
|
|
||||||
const pluginLabel = pluginBuilders[id as keyof PluginBuilderList]?.name ?? id;
|
const pluginLabel = allPlugins[id]?.name ?? id;
|
||||||
|
|
||||||
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
||||||
});
|
});
|
||||||
@ -106,7 +106,7 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Auto-update',
|
label: 'Auto-update',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.autoUpdates'),
|
checked: config.get('options.autoUpdates'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.autoUpdates', item.checked);
|
config.setMenuOption('options.autoUpdates', item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -114,21 +114,22 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Resume last song when app starts',
|
label: 'Resume last song when app starts',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.resumeOnStart'),
|
checked: config.get('options.resumeOnStart'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.resumeOnStart', item.checked);
|
config.setMenuOption('options.resumeOnStart', item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Starting page',
|
label: 'Starting page',
|
||||||
submenu: (() => {
|
submenu: (() => {
|
||||||
const subMenuArray: Electron.MenuItemConstructorOptions[] = Object.keys(startingPages).map((name) => ({
|
const subMenuArray: Electron.MenuItemConstructorOptions[] =
|
||||||
label: name,
|
Object.keys(startingPages).map((name) => ({
|
||||||
type: 'radio',
|
label: name,
|
||||||
checked: config.get('options.startingPage') === name,
|
type: 'radio',
|
||||||
click() {
|
checked: config.get('options.startingPage') === name,
|
||||||
config.set('options.startingPage', name);
|
click() {
|
||||||
},
|
config.set('options.startingPage', name);
|
||||||
}));
|
},
|
||||||
|
}));
|
||||||
subMenuArray.unshift({
|
subMenuArray.unshift({
|
||||||
label: 'Unset',
|
label: 'Unset',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
@ -147,8 +148,11 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Remove upgrade button',
|
label: 'Remove upgrade button',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.removeUpgradeButton'),
|
checked: config.get('options.removeUpgradeButton'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.removeUpgradeButton', item.checked);
|
config.setMenuOption(
|
||||||
|
'options.removeUpgradeButton',
|
||||||
|
item.checked,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -213,7 +217,7 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Single instance lock',
|
label: 'Single instance lock',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: true,
|
checked: true,
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
if (!item.checked && app.hasSingleInstanceLock()) {
|
if (!item.checked && app.hasSingleInstanceLock()) {
|
||||||
app.releaseSingleInstanceLock();
|
app.releaseSingleInstanceLock();
|
||||||
} else if (item.checked && !app.hasSingleInstanceLock()) {
|
} else if (item.checked && !app.hasSingleInstanceLock()) {
|
||||||
@ -225,43 +229,45 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Always on top',
|
label: 'Always on top',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.alwaysOnTop'),
|
checked: config.get('options.alwaysOnTop'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.alwaysOnTop', item.checked);
|
config.setMenuOption('options.alwaysOnTop', item.checked);
|
||||||
win.setAlwaysOnTop(item.checked);
|
win.setAlwaysOnTop(item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(is.windows() || is.linux()
|
...((is.windows() || is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: 'Hide menu',
|
label: 'Hide menu',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.hideMenu'),
|
checked: config.get('options.hideMenu'),
|
||||||
click(item) {
|
click(item) {
|
||||||
config.setMenuOption('options.hideMenu', item.checked);
|
config.setMenuOption('options.hideMenu', item.checked);
|
||||||
if (item.checked && !config.get('options.hideMenuWarned')) {
|
if (item.checked && !config.get('options.hideMenuWarned')) {
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info', title: 'Hide Menu Enabled',
|
type: 'info',
|
||||||
message: 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
|
title: 'Hide Menu Enabled',
|
||||||
});
|
message:
|
||||||
}
|
'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
]
|
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
||||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
...((is.windows() || is.macOS()
|
||||||
...(is.windows() || is.macOS()
|
|
||||||
? // Only works on Win/Mac
|
? // Only works on Win/Mac
|
||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: 'Start at login',
|
label: 'Start at login',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.startAtLogin'),
|
checked: config.get('options.startAtLogin'),
|
||||||
click(item) {
|
click(item) {
|
||||||
config.setMenuOption('options.startAtLogin', item.checked);
|
config.setMenuOption('options.startAtLogin', item.checked);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
]
|
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
||||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
|
||||||
{
|
{
|
||||||
label: 'Tray',
|
label: 'Tray',
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -277,7 +283,8 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
{
|
{
|
||||||
label: 'Enabled + app visible',
|
label: 'Enabled + app visible',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.get('options.tray') && config.get('options.appVisible'),
|
checked:
|
||||||
|
config.get('options.tray') && config.get('options.appVisible'),
|
||||||
click() {
|
click() {
|
||||||
config.setMenuOption('options.tray', true);
|
config.setMenuOption('options.tray', true);
|
||||||
config.setMenuOption('options.appVisible', true);
|
config.setMenuOption('options.appVisible', true);
|
||||||
@ -286,7 +293,8 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
{
|
{
|
||||||
label: 'Enabled + app hidden',
|
label: 'Enabled + app hidden',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.get('options.tray') && !config.get('options.appVisible'),
|
checked:
|
||||||
|
config.get('options.tray') && !config.get('options.appVisible'),
|
||||||
click() {
|
click() {
|
||||||
config.setMenuOption('options.tray', true);
|
config.setMenuOption('options.tray', true);
|
||||||
config.setMenuOption('options.appVisible', false);
|
config.setMenuOption('options.appVisible', false);
|
||||||
@ -297,8 +305,11 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Play/Pause on click',
|
label: 'Play/Pause on click',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.trayClickPlayPause'),
|
checked: config.get('options.trayClickPlayPause'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.trayClickPlayPause', item.checked);
|
config.setMenuOption(
|
||||||
|
'options.trayClickPlayPause',
|
||||||
|
item.checked,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -310,7 +321,7 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
{
|
{
|
||||||
label: 'Set Proxy',
|
label: 'Set Proxy',
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
async click(item) {
|
async click(item: MenuItem) {
|
||||||
await setProxy(item, win);
|
await setProxy(item, win);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -318,7 +329,7 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Override useragent',
|
label: 'Override useragent',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.overrideUserAgent'),
|
checked: config.get('options.overrideUserAgent'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.overrideUserAgent', item.checked);
|
config.setMenuOption('options.overrideUserAgent', item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -326,40 +337,46 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
label: 'Disable hardware acceleration',
|
label: 'Disable hardware acceleration',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.disableHardwareAcceleration'),
|
checked: config.get('options.disableHardwareAcceleration'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.disableHardwareAcceleration', item.checked);
|
config.setMenuOption(
|
||||||
|
'options.disableHardwareAcceleration',
|
||||||
|
item.checked,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Restart on config changes',
|
label: 'Restart on config changes',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.restartOnConfigChanges'),
|
checked: config.get('options.restartOnConfigChanges'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.restartOnConfigChanges', item.checked);
|
config.setMenuOption(
|
||||||
|
'options.restartOnConfigChanges',
|
||||||
|
item.checked,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Reset App cache when app starts',
|
label: 'Reset App cache when app starts',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.autoResetAppCache'),
|
checked: config.get('options.autoResetAppCache'),
|
||||||
click(item) {
|
click(item: MenuItem) {
|
||||||
config.setMenuOption('options.autoResetAppCache', item.checked);
|
config.setMenuOption('options.autoResetAppCache', item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
is.macOS()
|
is.macOS()
|
||||||
? {
|
? {
|
||||||
label: 'Toggle DevTools',
|
label: 'Toggle DevTools',
|
||||||
// Cannot use "toggleDevTools" role in macOS
|
// Cannot use "toggleDevTools" role in macOS
|
||||||
click() {
|
click() {
|
||||||
const { webContents } = win;
|
const { webContents } = win;
|
||||||
if (webContents.isDevToolsOpened()) {
|
if (webContents.isDevToolsOpened()) {
|
||||||
webContents.closeDevTools();
|
webContents.closeDevTools();
|
||||||
} else {
|
} else {
|
||||||
webContents.openDevTools();
|
webContents.openDevTools();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: { role: 'toggleDevTools' },
|
: { role: 'toggleDevTools' },
|
||||||
{
|
{
|
||||||
label: 'Edit config.json',
|
label: 'Edit config.json',
|
||||||
@ -377,8 +394,14 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
{ role: 'reload' },
|
{ role: 'reload' },
|
||||||
{ role: 'forceReload' },
|
{ role: 'forceReload' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'zoomIn', accelerator: process.platform === 'darwin' ? 'Cmd+I' : 'Ctrl+I' },
|
{
|
||||||
{ role: 'zoomOut', accelerator: process.platform === 'darwin' ? 'Cmd+O' : 'Ctrl+O' },
|
role: 'zoomIn',
|
||||||
|
accelerator: process.platform === 'darwin' ? 'Cmd+I' : 'Ctrl+I',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'zoomOut',
|
||||||
|
accelerator: process.platform === 'darwin' ? 'Cmd+O' : 'Ctrl+O',
|
||||||
|
},
|
||||||
{ role: 'resetZoom' },
|
{ role: 'resetZoom' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'togglefullscreen' },
|
{ role: 'togglefullscreen' },
|
||||||
@ -419,14 +442,12 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'About',
|
label: 'About',
|
||||||
submenu: [
|
submenu: [{ role: 'about' }],
|
||||||
{ role: 'about' },
|
},
|
||||||
],
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
|
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||||
const menuTemplate: MenuTemplate = [...await mainMenuTemplate(win)];
|
const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const { name } = app;
|
const { name } = app;
|
||||||
menuTemplate.unshift({
|
menuTemplate.unshift({
|
||||||
@ -455,23 +476,27 @@ export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
||||||
const output = await prompt({
|
const output = await prompt(
|
||||||
title: 'Set Proxy',
|
{
|
||||||
label: 'Enter Proxy Address: (leave empty to disable)',
|
title: 'Set Proxy',
|
||||||
value: config.get('options.proxy'),
|
label: 'Enter Proxy Address: (leave empty to disable)',
|
||||||
type: 'input',
|
value: config.get('options.proxy'),
|
||||||
inputAttrs: {
|
type: 'input',
|
||||||
type: 'url',
|
inputAttrs: {
|
||||||
placeholder: "Example: 'socks5://127.0.0.1:9999",
|
type: 'url',
|
||||||
|
placeholder: "Example: 'socks5://127.0.0.1:9999",
|
||||||
|
},
|
||||||
|
width: 450,
|
||||||
|
...promptOptions(),
|
||||||
},
|
},
|
||||||
width: 450,
|
win,
|
||||||
...promptOptions(),
|
);
|
||||||
}, win);
|
|
||||||
|
|
||||||
if (typeof output === 'string') {
|
if (typeof output === 'string') {
|
||||||
config.setMenuOption('options.proxy', output);
|
config.setMenuOption('options.proxy', output);
|
||||||
item.checked = output !== '';
|
item.checked = output !== '';
|
||||||
} else { // User pressed cancel
|
} else {
|
||||||
|
// User pressed cancel
|
||||||
item.checked = !item.checked; // Reset checkbox
|
item.checked = !item.checked; // Reset checkbox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/plugins/audio-compressor.ts
Normal file
25
src/plugins/audio-compressor.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
|
name: 'Audio Compressor',
|
||||||
|
description: '',
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
document.addEventListener(
|
||||||
|
'audioCanPlay',
|
||||||
|
({ detail: { audioSource, audioContext } }) => {
|
||||||
|
const compressor = audioContext.createDynamicsCompressor();
|
||||||
|
|
||||||
|
compressor.threshold.value = -50;
|
||||||
|
compressor.ratio.value = 12;
|
||||||
|
compressor.knee.value = 40;
|
||||||
|
compressor.attack.value = 0;
|
||||||
|
compressor.release.value = 0.25;
|
||||||
|
|
||||||
|
audioSource.connect(compressor);
|
||||||
|
compressor.connect(audioContext.destination);
|
||||||
|
},
|
||||||
|
{ once: true, passive: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { createPluginBuilder } from '../utils/builder';
|
|
||||||
|
|
||||||
const builder = createPluginBuilder('audio-compressor', {
|
|
||||||
name: 'Audio Compressor',
|
|
||||||
restartNeeded: false,
|
|
||||||
config: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default builder;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface PluginBuilderList {
|
|
||||||
[builder.id]: typeof builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import builder from './index';
|
|
||||||
|
|
||||||
export default builder.createRenderer(() => {
|
|
||||||
return {
|
|
||||||
onLoad() {
|
|
||||||
document.addEventListener('audioCanPlay', (e) => {
|
|
||||||
const { audioContext } = e.detail;
|
|
||||||
|
|
||||||
const compressor = audioContext.createDynamicsCompressor();
|
|
||||||
compressor.threshold.value = -50;
|
|
||||||
compressor.ratio.value = 12;
|
|
||||||
compressor.knee.value = 40;
|
|
||||||
compressor.attack.value = 0;
|
|
||||||
compressor.release.value = 0.25;
|
|
||||||
|
|
||||||
e.detail.audioSource.connect(compressor);
|
|
||||||
compressor.connect(audioContext.destination);
|
|
||||||
}, {
|
|
||||||
once: true, // Only create the audio compressor once, not on each video
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@ -1,20 +1,7 @@
|
|||||||
|
import { createPlugin } from '@/utils';
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
import { createPluginBuilder } from '../utils/builder';
|
export default createPlugin({
|
||||||
|
|
||||||
const builder = createPluginBuilder('blur-nav-bar', {
|
|
||||||
name: 'Blur Navigation Bar',
|
name: 'Blur Navigation Bar',
|
||||||
restartNeeded: true,
|
renderer: { stylesheet: style },
|
||||||
config: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
styles: [style],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default builder;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface PluginBuilderList {
|
|
||||||
[builder.id]: typeof builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,17 +1,9 @@
|
|||||||
import { createPluginBuilder } from '../utils/builder';
|
import { createPlugin } from '@/utils';
|
||||||
|
|
||||||
const builder = createPluginBuilder('bypass-age-restrictions', {
|
export default createPlugin({
|
||||||
name: 'Bypass Age Restrictions',
|
name: 'Bypass Age Restrictions',
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
config: {
|
|
||||||
enabled: false,
|
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||||
},
|
renderer: () => import('simple-youtube-age-restriction-bypass'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default builder;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface PluginBuilderList {
|
|
||||||
[builder.id]: typeof builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import builder from './index';
|
|
||||||
|
|
||||||
export default builder.createRenderer(() => ({
|
|
||||||
async onLoad() {
|
|
||||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
|
||||||
await import('simple-youtube-age-restriction-bypass');
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@ -1,20 +1,54 @@
|
|||||||
import { createPluginBuilder } from '../utils/builder';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
const builder = createPluginBuilder('captions-selector', {
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
name: 'Captions Selector',
|
name: 'Captions Selector',
|
||||||
restartNeeded: false,
|
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
|
||||||
disableCaptions: false,
|
disableCaptions: false,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
lastCaptionsCode: '',
|
lastCaptionsCode: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
menu({ getConfig, setConfig }) {
|
||||||
|
const config = getConfig();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Automatically select last used caption',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: config.autoload as boolean,
|
||||||
|
click(item) {
|
||||||
|
setConfig({ autoload: item.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No captions by default',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: config.disableCaptions as boolean,
|
||||||
|
click(item) {
|
||||||
|
setConfig({ disableCaptions: item.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
backend({ ipc: { handle }, win }) {
|
||||||
|
handle(
|
||||||
|
'captionsSelector',
|
||||||
|
async (_, captionLabels: Record<string, string>, currentIndex: string) =>
|
||||||
|
await prompt(
|
||||||
|
{
|
||||||
|
title: 'Choose Caption',
|
||||||
|
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
||||||
|
type: 'select',
|
||||||
|
value: currentIndex,
|
||||||
|
selectOptions: captionLabels,
|
||||||
|
resizable: true,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
win,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default builder;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface PluginBuilderList {
|
|
||||||
[builder.id]: typeof builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import prompt from 'custom-electron-prompt';
|
|
||||||
|
|
||||||
import builder from './index';
|
|
||||||
|
|
||||||
import promptOptions from '../../providers/prompt-options';
|
|
||||||
|
|
||||||
export default builder.createMain(({ handle }) => ({
|
|
||||||
onLoad(window) {
|
|
||||||
handle('captionsSelector', async (_, captionLabels: Record<string, string>, currentIndex: string) => await prompt(
|
|
||||||
{
|
|
||||||
title: 'Choose Caption',
|
|
||||||
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
|
||||||
type: 'select',
|
|
||||||
value: currentIndex,
|
|
||||||
selectOptions: captionLabels,
|
|
||||||
resizable: true,
|
|
||||||
...promptOptions(),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import builder from './index';
|
|
||||||
|
|
||||||
import type { MenuTemplate } from '../../menu';
|
|
||||||
|
|
||||||
export default builder.createMenu(async ({ getConfig, setConfig }): Promise<MenuTemplate> => {
|
|
||||||
const config = await getConfig();
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Automatically select last used caption',
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: config.autoload,
|
|
||||||
click(item) {
|
|
||||||
setConfig({ autoload: item.checked });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'No captions by default',
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: config.disableCaptions,
|
|
||||||
click(item) {
|
|
||||||
setConfig({ disableCaptions: item.checked });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
export * from './css';
|
export * from './css';
|
||||||
export * from './fs';
|
export * from './fs';
|
||||||
export * from './plugin';
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './fetch';
|
export * from './fetch';
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
import is from 'electron-is';
|
|
||||||
|
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
|
||||||
|
|
||||||
export const getAvailablePluginNames = () => {
|
|
||||||
return Object.keys(pluginBuilders)
|
|
||||||
.sort()
|
|
||||||
.filter((id) => {
|
|
||||||
if (is.windows() && id === 'touchbar') {
|
|
||||||
return false;
|
|
||||||
} else if (is.macOS() && id === 'taskbar-mediacontrol') {
|
|
||||||
return false;
|
|
||||||
} else if (is.linux() && (id === 'taskbar-mediacontrol' || id === 'touchbar')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,29 +1,14 @@
|
|||||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
|
||||||
import { preloadPlugins } from 'virtual:PreloadPlugins';
|
|
||||||
|
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
import {
|
|
||||||
PluginBaseConfig,
|
|
||||||
PluginBuilder,
|
|
||||||
PreloadPluginFactory
|
|
||||||
} from './plugins/utils/builder';
|
|
||||||
import {
|
import {
|
||||||
forceLoadPreloadPlugin,
|
forceLoadPreloadPlugin,
|
||||||
forceUnloadPreloadPlugin,
|
forceUnloadPreloadPlugin,
|
||||||
loadAllPreloadPlugins,
|
loadAllPreloadPlugins,
|
||||||
registerPreloadPlugin
|
|
||||||
} from './loader/preload';
|
} from './loader/preload';
|
||||||
|
|
||||||
Object.entries(pluginBuilders).forEach(([id, builder]) => {
|
|
||||||
const typedBuilder = builder as PluginBuilder<string, PluginBaseConfig>;
|
|
||||||
const plugin = preloadPlugins[id] as PreloadPluginFactory<PluginBaseConfig> | undefined;
|
|
||||||
|
|
||||||
registerPreloadPlugin(id, typedBuilder, plugin);
|
|
||||||
});
|
|
||||||
loadAllPreloadPlugins();
|
loadAllPreloadPlugins();
|
||||||
|
|
||||||
ipcRenderer.on('plugin:unload', (_, id: keyof PluginBuilderList) => {
|
ipcRenderer.on('plugin:unload', (_, id: keyof PluginBuilderList) => {
|
||||||
@ -33,19 +18,34 @@ ipcRenderer.on('plugin:enable', (_, id: keyof PluginBuilderList) => {
|
|||||||
forceLoadPreloadPlugin(id);
|
forceLoadPreloadPlugin(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('mainConfig', config);
|
contextBridge.exposeInMainWorld('mainConfig', config);
|
||||||
contextBridge.exposeInMainWorld('electronIs', is);
|
contextBridge.exposeInMainWorld('electronIs', is);
|
||||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||||
on: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.on(channel, listener),
|
on: (
|
||||||
off: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.off(channel, listener),
|
channel: string,
|
||||||
once: (channel: string, listener: (event: IpcRendererEvent, ...args: unknown[]) => void) => ipcRenderer.once(channel, listener),
|
listener: (event: IpcRendererEvent, ...args: unknown[]) => void,
|
||||||
send: (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args),
|
) => ipcRenderer.on(channel, listener),
|
||||||
removeListener: (channel: string, listener: (...args: unknown[]) => void) => ipcRenderer.removeListener(channel, listener),
|
off: (channel: string, listener: (...args: unknown[]) => void) =>
|
||||||
removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel),
|
ipcRenderer.off(channel, listener),
|
||||||
invoke: async (channel: string, ...args: unknown[]): Promise<unknown> => ipcRenderer.invoke(channel, ...args),
|
once: (
|
||||||
sendSync: (channel: string, ...args: unknown[]): unknown => ipcRenderer.sendSync(channel, ...args),
|
channel: string,
|
||||||
sendToHost: (channel: string, ...args: unknown[]) => ipcRenderer.sendToHost(channel, ...args),
|
listener: (event: IpcRendererEvent, ...args: unknown[]) => void,
|
||||||
|
) => ipcRenderer.once(channel, listener),
|
||||||
|
send: (channel: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.send(channel, ...args),
|
||||||
|
removeListener: (channel: string, listener: (...args: unknown[]) => void) =>
|
||||||
|
ipcRenderer.removeListener(channel, listener),
|
||||||
|
removeAllListeners: (channel: string) =>
|
||||||
|
ipcRenderer.removeAllListeners(channel),
|
||||||
|
invoke: async (channel: string, ...args: unknown[]): Promise<unknown> =>
|
||||||
|
ipcRenderer.invoke(channel, ...args),
|
||||||
|
sendSync: (channel: string, ...args: unknown[]): unknown =>
|
||||||
|
ipcRenderer.sendSync(channel, ...args),
|
||||||
|
sendToHost: (channel: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.sendToHost(channel, ...args),
|
||||||
});
|
});
|
||||||
contextBridge.exposeInMainWorld('reload', () => ipcRenderer.send('reload'));
|
contextBridge.exposeInMainWorld('reload', () => ipcRenderer.send('reload'));
|
||||||
contextBridge.exposeInMainWorld('ELECTRON_RENDERER_URL', process.env.ELECTRON_RENDERER_URL);
|
contextBridge.exposeInMainWorld(
|
||||||
|
'ELECTRON_RENDERER_URL',
|
||||||
|
process.env.ELECTRON_RENDERER_URL,
|
||||||
|
);
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
import { rendererPlugins } from 'virtual:RendererPlugins';
|
import { rendererPlugins } from 'virtual:plugins';
|
||||||
import { pluginBuilders } from 'virtual:PluginBuilders';
|
|
||||||
|
|
||||||
import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugins/utils/builder';
|
import {
|
||||||
|
PluginBaseConfig,
|
||||||
|
PluginBuilder,
|
||||||
|
RendererPluginFactory,
|
||||||
|
} from '@/plugins/utils/builder';
|
||||||
|
|
||||||
import { startingPages } from './providers/extracted-data';
|
import { startingPages } from './providers/extracted-data';
|
||||||
import setupSongInfo from './providers/song-info-front';
|
import setupSongInfo from './providers/song-info-front';
|
||||||
import {
|
import {
|
||||||
forceLoadRendererPlugin,
|
forceLoadRendererPlugin,
|
||||||
forceUnloadRendererPlugin,
|
forceUnloadRendererPlugin,
|
||||||
getAllLoadedRendererPlugins, getLoadedRendererPlugin,
|
getAllLoadedRendererPlugins,
|
||||||
|
getLoadedRendererPlugin,
|
||||||
loadAllRendererPlugins,
|
loadAllRendererPlugins,
|
||||||
registerRendererPlugin
|
|
||||||
} from './loader/renderer';
|
} from './loader/renderer';
|
||||||
|
|
||||||
import type { YoutubePlayer } from './types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
let api: (Element & YoutubePlayer) | null = null;
|
let api: (Element & YoutubePlayer) | null = null;
|
||||||
|
|
||||||
@ -34,7 +37,10 @@ function listenForApiLoad() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface YouTubeMusicAppElement extends HTMLElement {
|
interface YouTubeMusicAppElement extends HTMLElement {
|
||||||
@ -75,17 +81,14 @@ function onApiLoaded() {
|
|||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.values(getAllLoadedRendererPlugins())
|
|
||||||
.forEach((plugin) => {
|
|
||||||
plugin.onPlayerApiReady?.(api!);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.send('ytmd:player-api-loaded');
|
window.ipcRenderer.send('ytmd:player-api-loaded');
|
||||||
|
|
||||||
// Navigate to "Starting page"
|
// Navigate to "Starting page"
|
||||||
const startingPage: string = window.mainConfig.get('options.startingPage');
|
const startingPage: string = window.mainConfig.get('options.startingPage');
|
||||||
if (startingPage && startingPages[startingPage]) {
|
if (startingPage && startingPages[startingPage]) {
|
||||||
document.querySelector<YouTubeMusicAppElement>('ytmusic-app')?.navigate_(startingPages[startingPage]);
|
document
|
||||||
|
.querySelector<YouTubeMusicAppElement>('ytmusic-app')
|
||||||
|
?.navigate_(startingPages[startingPage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove upgrade button
|
// Remove upgrade button
|
||||||
@ -98,51 +101,54 @@ function onApiLoaded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide / Force show like buttons
|
// Hide / Force show like buttons
|
||||||
const likeButtonsOptions: string = window.mainConfig.get('options.likeButtons');
|
const likeButtonsOptions: string = window.mainConfig.get(
|
||||||
|
'options.likeButtons',
|
||||||
|
);
|
||||||
if (likeButtonsOptions) {
|
if (likeButtonsOptions) {
|
||||||
const likeButtons: HTMLElement | null = document.querySelector('ytmusic-like-button-renderer');
|
const likeButtons: HTMLElement | null = document.querySelector(
|
||||||
|
'ytmusic-like-button-renderer',
|
||||||
|
);
|
||||||
if (likeButtons) {
|
if (likeButtons) {
|
||||||
likeButtons.style.display
|
likeButtons.style.display =
|
||||||
= {
|
{
|
||||||
hide: 'none',
|
hide: 'none',
|
||||||
force: 'inherit',
|
force: 'inherit',
|
||||||
}[likeButtonsOptions] || '';
|
}[likeButtonsOptions] || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(() => {
|
||||||
Object.entries(pluginBuilders).forEach(([id, builder]) => {
|
loadAllRendererPlugins();
|
||||||
const typedBuilder = builder as PluginBuilder<string, PluginBaseConfig>;
|
|
||||||
const plugin = rendererPlugins[id] as RendererPluginFactory<PluginBaseConfig> | undefined;
|
|
||||||
|
|
||||||
registerRendererPlugin(id, typedBuilder, plugin);
|
window.ipcRenderer.on(
|
||||||
});
|
'plugin:unload',
|
||||||
await loadAllRendererPlugins();
|
(_event, id: keyof PluginBuilderList) => {
|
||||||
|
forceUnloadRendererPlugin(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.ipcRenderer.on(
|
||||||
|
'plugin:enable',
|
||||||
|
(_event, id: keyof PluginBuilderList) => {
|
||||||
|
forceLoadRendererPlugin(id);
|
||||||
|
if (api) {
|
||||||
|
const plugin = getLoadedRendererPlugin(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
window.ipcRenderer.on('plugin:unload', (_event, id: keyof PluginBuilderList) => {
|
window.ipcRenderer.on(
|
||||||
forceUnloadRendererPlugin(id);
|
'config-changed',
|
||||||
});
|
(_event, id: string, newConfig: PluginBaseConfig) => {
|
||||||
window.ipcRenderer.on('plugin:enable', async (_event, id: keyof PluginBuilderList) => {
|
const plugin = getAllLoadedRendererPlugins()[id];
|
||||||
await forceLoadRendererPlugin(id);
|
},
|
||||||
if (api) {
|
);
|
||||||
const plugin = getLoadedRendererPlugin(id);
|
|
||||||
|
|
||||||
if (plugin) plugin.onPlayerApiReady?.(api);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => {
|
|
||||||
const plugin = getAllLoadedRendererPlugins()[id];
|
|
||||||
|
|
||||||
if (plugin) plugin.onConfigChange?.(newConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for complete load of YouTube api
|
// Wait for complete load of YouTube api
|
||||||
listenForApiLoad();
|
listenForApiLoad();
|
||||||
|
|
||||||
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||||
setInterval(() => window._lact = Date.now(), 900_000);
|
setInterval(() => (window._lact = Date.now()), 900_000);
|
||||||
|
|
||||||
// Setup back to front logger
|
// Setup back to front logger
|
||||||
if (window.electronIs.dev()) {
|
if (window.electronIs.dev()) {
|
||||||
|
|||||||
33
src/types/contexts.ts
Normal file
33
src/types/contexts.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { IpcMain, IpcRenderer, BrowserWindow } from 'electron';
|
||||||
|
import type { PluginConfig } from '@/types/plugins';
|
||||||
|
|
||||||
|
export interface BackendContext {
|
||||||
|
getConfig(): PluginConfig;
|
||||||
|
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
|
||||||
|
|
||||||
|
ipc: {
|
||||||
|
handle: (event: string, listener: CallableFunction) => void;
|
||||||
|
on: (event: string, listener: CallableFunction) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
win: BrowserWindow;
|
||||||
|
electron: typeof import('electron');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuContext {
|
||||||
|
getConfig(): PluginConfig;
|
||||||
|
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
|
||||||
|
|
||||||
|
window: BrowserWindow;
|
||||||
|
refresh: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreloadContext {
|
||||||
|
getConfig(): PluginConfig;
|
||||||
|
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RendererContext {
|
||||||
|
getConfig(): PluginConfig;
|
||||||
|
setConfig(conf: Omit<PluginConfig, 'enabled'>): void;
|
||||||
|
}
|
||||||
38
src/types/plugins.ts
Normal file
38
src/types/plugins.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type {
|
||||||
|
BackendContext,
|
||||||
|
MenuContext,
|
||||||
|
PreloadContext,
|
||||||
|
RendererContext,
|
||||||
|
} from './contexts';
|
||||||
|
|
||||||
|
type Author = string;
|
||||||
|
|
||||||
|
export type PluginConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type PluginExtra = Record<string, any>;
|
||||||
|
|
||||||
|
type PluginLifecycle<T> =
|
||||||
|
| ((ctx: T) => void | Promise<void>)
|
||||||
|
| ({
|
||||||
|
start?(ctx: T): void | Promise<void>;
|
||||||
|
stop?(ctx: T): void | Promise<void>;
|
||||||
|
} & PluginExtra);
|
||||||
|
|
||||||
|
export interface PluginDef {
|
||||||
|
name: string;
|
||||||
|
authors?: Author[];
|
||||||
|
description?: string;
|
||||||
|
config: PluginConfig;
|
||||||
|
|
||||||
|
menu?: (ctx: MenuContext) => Electron.MenuItemConstructorOptions[];
|
||||||
|
restartNeeded?: boolean;
|
||||||
|
|
||||||
|
backend?: PluginLifecycle<BackendContext>;
|
||||||
|
preload?: PluginLifecycle<PreloadContext>;
|
||||||
|
renderer?: PluginLifecycle<RendererContext> & {
|
||||||
|
stylesheet?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
64
src/utils/index.ts
Normal file
64
src/utils/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
BackendContext,
|
||||||
|
PreloadContext,
|
||||||
|
RendererContext,
|
||||||
|
} from '@/types/contexts';
|
||||||
|
import { PluginDef, PluginConfig } from '@/types/plugins';
|
||||||
|
|
||||||
|
export const createPlugin = (
|
||||||
|
def: Omit<PluginDef, 'config'> & {
|
||||||
|
config?: Omit<PluginConfig, 'enabled'>;
|
||||||
|
},
|
||||||
|
): PluginDef => def as PluginDef;
|
||||||
|
|
||||||
|
type Options =
|
||||||
|
| { ctx: 'backend'; context: BackendContext }
|
||||||
|
| { ctx: 'preload'; context: PreloadContext }
|
||||||
|
| { ctx: 'renderer'; context: RendererContext };
|
||||||
|
|
||||||
|
export const startPlugin = (id: string, def: PluginDef, options: Options) => {
|
||||||
|
const lifecycle: (ctx: (typeof options)['context']) => void =
|
||||||
|
typeof def[options.ctx] === 'function'
|
||||||
|
? def[options.ctx]
|
||||||
|
: // @ts-expect-error TS is dum dum
|
||||||
|
def[options.ctx]?.start;
|
||||||
|
|
||||||
|
if (!lifecycle) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const start = performance.now();
|
||||||
|
lifecycle(options.context);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[YTM] Failed to start ${id}::${options.ctx}: ${String(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
`[YTM] Failed to execute ${id}::${options.ctx}: ${String(err)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
36
src/virtual-module.d.ts
vendored
36
src/virtual-module.d.ts
vendored
@ -1,27 +1,13 @@
|
|||||||
declare module 'virtual:MainPlugins' {
|
declare module 'virtual:plugins' {
|
||||||
import type { MainPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
|
import type { PluginDef } from '@/types/plugins';
|
||||||
|
|
||||||
export const mainPlugins: Record<string, MainPluginFactory<PluginBaseConfig>>;
|
export const mainPlugins: Record<string, PluginDef>;
|
||||||
|
export const menuPlugins: Record<string, PluginDef>;
|
||||||
|
export const preloadPlugins: Record<string, PluginDef>;
|
||||||
|
export const rendererPlugins: Record<string, PluginDef>;
|
||||||
|
|
||||||
|
export const allPlugins: Record<
|
||||||
|
string,
|
||||||
|
Omit<PluginDef, 'backend' | 'preload' | 'renderer'>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'virtual:MenuPlugins' {
|
|
||||||
import type { MenuPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
|
|
||||||
|
|
||||||
export const menuPlugins: Record<string, MenuPluginFactory<PluginBaseConfig>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'virtual:PreloadPlugins' {
|
|
||||||
import type { PreloadPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
|
|
||||||
|
|
||||||
export const preloadPlugins: Record<string, PreloadPluginFactory<PluginBaseConfig>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'virtual:RendererPlugins' {
|
|
||||||
import type { RendererPluginFactory, PluginBaseConfig } from './plugins/utils/builder';
|
|
||||||
|
|
||||||
export const rendererPlugins: Record<string, RendererPluginFactory<PluginBaseConfig>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'virtual:PluginBuilders' {
|
|
||||||
export const pluginBuilders: PluginBuilderList;
|
|
||||||
}
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["dom", "dom.iterable", "es2022"],
|
"lib": ["dom", "dom.iterable", "es2022"],
|
||||||
@ -8,15 +9,17 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"baseUrl": "./src",
|
"baseUrl": ".",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@assets": ["./assets/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["./dist"],
|
"exclude": ["./dist"],
|
||||||
"paths": {
|
"include": ["electron.vite.config.ts", "./src/**/*"]
|
||||||
"*": ["*.d.ts"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
63
vite-plugins/plugin-importer.ts
Normal file
63
vite-plugins/plugin-importer.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { basename, relative, resolve, extname } from 'node:path';
|
||||||
|
|
||||||
|
import { globSync } from 'glob';
|
||||||
|
import { Project } from 'ts-morph';
|
||||||
|
|
||||||
|
const snakeToCamel = (text: string) =>
|
||||||
|
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
||||||
|
|
||||||
|
export const pluginVirtualModuleGenerator = (
|
||||||
|
mode: 'main' | 'preload' | 'renderer' | 'menu',
|
||||||
|
) => {
|
||||||
|
const project = new Project({
|
||||||
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
|
skipAddingFilesFromTsConfig: true,
|
||||||
|
skipLoadingLibFiles: true,
|
||||||
|
skipFileDependencyResolution: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const srcPath = resolve(__dirname, '..', 'src');
|
||||||
|
const plugins = globSync([
|
||||||
|
'src/plugins/*/index.{js,ts}',
|
||||||
|
'src/plugins/*.{js,ts}',
|
||||||
|
'!src/plugins/utils/**/*',
|
||||||
|
'!src/plugins/utils/*',
|
||||||
|
]).map((path) => {
|
||||||
|
let name = basename(path);
|
||||||
|
if (name === 'index.ts' || name === 'index.js') {
|
||||||
|
name = basename(resolve(path, '..'));
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.replace(extname(name), '');
|
||||||
|
|
||||||
|
return { name, path };
|
||||||
|
});
|
||||||
|
|
||||||
|
const src = project.createSourceFile('vm:pluginIndexes', (writer) => {
|
||||||
|
// prettier-ignore
|
||||||
|
for (const { name, path } of plugins) {
|
||||||
|
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
||||||
|
writer.writeLine(`import ${snakeToCamel(name)}Plugin from "./${relativePath}";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.blankLine();
|
||||||
|
|
||||||
|
// Context-specific exports
|
||||||
|
writer.writeLine(`export const ${mode}Plugins = {`);
|
||||||
|
for (const { name } of plugins) {
|
||||||
|
writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`);
|
||||||
|
}
|
||||||
|
writer.writeLine('};');
|
||||||
|
writer.blankLine();
|
||||||
|
|
||||||
|
// All plugins export
|
||||||
|
writer.writeLine('export const allPlugins = {');
|
||||||
|
for (const { name } of plugins) {
|
||||||
|
writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`);
|
||||||
|
}
|
||||||
|
writer.writeLine('};');
|
||||||
|
writer.blankLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
return src.getText();
|
||||||
|
};
|
||||||
105
vite-plugins/plugin-loader.ts
Normal file
105
vite-plugins/plugin-loader.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve, basename } from 'node:path';
|
||||||
|
|
||||||
|
import { createFilter } from 'vite';
|
||||||
|
import { Project, ts, ObjectLiteralExpression } from 'ts-morph';
|
||||||
|
|
||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
|
||||||
|
export default function (mode: 'backend' | 'preload' | 'renderer' | 'none') {
|
||||||
|
const pluginFilter = createFilter([
|
||||||
|
'src/plugins/*/index.{js,ts}',
|
||||||
|
'src/plugins/*',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <PluginOption>{
|
||||||
|
name: 'ytm-plugin-loader',
|
||||||
|
async load(id) {
|
||||||
|
if (!pluginFilter(id)) return null;
|
||||||
|
|
||||||
|
const project = new Project({
|
||||||
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
|
skipAddingFilesFromTsConfig: true,
|
||||||
|
skipLoadingLibFiles: true,
|
||||||
|
skipFileDependencyResolution: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const src = project.createSourceFile(
|
||||||
|
'_pf' + basename(id),
|
||||||
|
await readFile(id, 'utf8'),
|
||||||
|
);
|
||||||
|
const exports = src.getExportedDeclarations();
|
||||||
|
let objExpr: ObjectLiteralExpression | undefined = undefined;
|
||||||
|
|
||||||
|
for (const [name, [expr]] of exports) {
|
||||||
|
if (name !== 'default') continue;
|
||||||
|
|
||||||
|
switch (expr.getKind()) {
|
||||||
|
case ts.SyntaxKind.ObjectLiteralExpression: {
|
||||||
|
objExpr = expr.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ts.SyntaxKind.CallExpression: {
|
||||||
|
const callExpr = expr.asKindOrThrow(ts.SyntaxKind.CallExpression);
|
||||||
|
if (callExpr.getArguments().length !== 1) continue;
|
||||||
|
|
||||||
|
const name = callExpr.getExpression().getText();
|
||||||
|
if (name !== 'createPlugin') continue;
|
||||||
|
|
||||||
|
const arg = callExpr.getArguments()[0];
|
||||||
|
if (arg.getKind() !== ts.SyntaxKind.ObjectLiteralExpression)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
objExpr = arg.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!objExpr) return null;
|
||||||
|
|
||||||
|
const properties = objExpr.getProperties();
|
||||||
|
const propertyNames = properties.map((prop) => {
|
||||||
|
switch (prop.getKind()) {
|
||||||
|
case ts.SyntaxKind.PropertyAssignment:
|
||||||
|
return prop
|
||||||
|
.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
||||||
|
.getName();
|
||||||
|
case ts.SyntaxKind.ShorthandPropertyAssignment:
|
||||||
|
return prop
|
||||||
|
.asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment)
|
||||||
|
.getName();
|
||||||
|
case ts.SyntaxKind.MethodDeclaration:
|
||||||
|
return prop
|
||||||
|
.asKindOrThrow(ts.SyntaxKind.MethodDeclaration)
|
||||||
|
.getName();
|
||||||
|
default:
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const contexts = ['backend', 'preload', 'renderer'];
|
||||||
|
for (const ctx of contexts) {
|
||||||
|
if (mode === 'none') {
|
||||||
|
const index = propertyNames.indexOf(ctx);
|
||||||
|
if (index === -1) continue;
|
||||||
|
|
||||||
|
objExpr.getProperty(propertyNames[index])?.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx === mode) continue;
|
||||||
|
|
||||||
|
const index = propertyNames.indexOf(ctx);
|
||||||
|
if (index === -1) continue;
|
||||||
|
|
||||||
|
objExpr.getProperty(propertyNames[index])?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: src.getText(),
|
||||||
|
ast: src,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { existsSync } from 'node:fs';
|
|
||||||
import { basename, relative, resolve } from 'node:path';
|
|
||||||
|
|
||||||
import { globSync } from 'glob';
|
|
||||||
|
|
||||||
type PluginType = 'index' | 'main' | 'preload' | 'renderer' | 'menu';
|
|
||||||
|
|
||||||
const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
|
||||||
const getName = (mode: PluginType, name: string) => {
|
|
||||||
if (mode === 'index') {
|
|
||||||
return snakeToCamel(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${snakeToCamel(name)}Plugin`;
|
|
||||||
};
|
|
||||||
const getListName = (mode: PluginType) => {
|
|
||||||
if (mode === 'index') return 'pluginBuilders';
|
|
||||||
|
|
||||||
return `${mode}Plugins`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pluginVirtualModuleGenerator = (mode: PluginType) => {
|
|
||||||
const srcPath = resolve(__dirname, '..', 'src');
|
|
||||||
|
|
||||||
const plugins = globSync(`${srcPath}/plugins/*`)
|
|
||||||
.map((path) => ({ name: basename(path), path }))
|
|
||||||
.filter(({ name, path }) => {
|
|
||||||
if (name.startsWith('utils')) return false;
|
|
||||||
|
|
||||||
return existsSync(resolve(path, `${mode}.ts`)) || (mode !== 'index' && existsSync(resolve(path, `${mode}`, 'index.ts')));
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('converted plugin list');
|
|
||||||
console.log(plugins.map((it) => it.name));
|
|
||||||
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
for (const { name, path } of plugins) {
|
|
||||||
result += `import ${getName(mode, name)} from "./${relative(resolve(srcPath, '..'), path).replace(/\\/g, '/')}/${mode}";\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
result += `export const ${getListName(mode)} = {\n`;
|
|
||||||
for (const { name } of plugins) {
|
|
||||||
result += ` "${name}": ${getName(mode, name)},\n`;
|
|
||||||
}
|
|
||||||
result += '};';
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user