Compare commits

...

8 Commits

Author SHA1 Message Date
b53ece5836 feat: code splitting (#3593)
Co-authored-by: Angelos Bouklis <me@arjix.dev>
2025-07-12 00:00:03 +09:00
c04dc92d39 fix(deps): update dependency @ghostery/adblocker-electron-preload to v2.11.1 (#3571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:59:03 +09:00
78acd2ddbb fix(deps): update dependency @ghostery/adblocker-electron to v2.11.1 (#3570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:57:09 +09:00
7126c290e2 chore(deps): update playwright monorepo to v1.54.1 (#3599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:56:56 +09:00
58651857e2 fix: remove debug log 2025-07-11 23:44:02 +09:00
8e7c95e68f chore(deps): update playwright monorepo to v1.54.0 (#3591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:42:39 +09:00
1c68c5637d chore(deps): update dependency electron to v37.2.1 (#3559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:42:13 +09:00
c44d5ea111 fix: improve menu detector 2025-07-11 23:04:00 +09:00
21 changed files with 304 additions and 206 deletions

View File

@ -68,8 +68,8 @@
"@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.7.2",
"@foobar404/wave": "2.0.5",
"@ghostery/adblocker-electron": "2.11.0",
"@ghostery/adblocker-electron-preload": "2.11.0",
"@ghostery/adblocker-electron": "2.11.1",
"@ghostery/adblocker-electron-preload": "2.11.1",
"@hono/node-server": "1.15.0",
"@hono/swagger-ui": "0.5.2",
"@hono/zod-openapi": "0.19.9",
@ -138,7 +138,7 @@
"devDependencies": {
"@eslint/js": "9.30.1",
"@malept/flatpak-bundler": "0.4.0",
"@playwright/test": "1.53.2",
"@playwright/test": "1.54.1",
"@stylistic/eslint-plugin": "5.1.0",
"@total-typescript/ts-reset": "0.6.1",
"@types/electron-localshortcut": "3.1.3",
@ -151,7 +151,7 @@
"cross-env": "7.0.3",
"del-cli": "6.0.0",
"discord-api-types": "0.38.15",
"electron": "37.2.0",
"electron": "37.2.1",
"electron-builder": "26.0.12",
"electron-builder-squirrel-windows": "26.0.12",
"electron-devtools-installer": "4.0.0",
@ -166,7 +166,7 @@
"eslint-plugin-solid": "0.14.5",
"glob": "11.0.3",
"node-gyp": "11.2.0",
"playwright": "1.53.2",
"playwright": "1.54.1",
"rollup": "4.44.2",
"typescript": "5.8.3",
"typescript-eslint": "8.36.0",

108
pnpm-lock.yaml generated
View File

@ -36,7 +36,7 @@ importers:
version: 1.0.1(@types/node@22.13.5)
'@electron/remote':
specifier: 2.1.3
version: 2.1.3(electron@37.2.0)
version: 2.1.3(electron@37.2.1)
'@ffmpeg.wasm/core-mt':
specifier: 0.12.0
version: 0.12.0
@ -50,11 +50,11 @@ importers:
specifier: 2.0.5
version: 2.0.5
'@ghostery/adblocker-electron':
specifier: 2.11.0
version: 2.11.0(electron@37.2.0)
specifier: 2.11.1
version: 2.11.1(electron@37.2.1)
'@ghostery/adblocker-electron-preload':
specifier: 2.11.0
version: 2.11.0(electron@37.2.0)
specifier: 2.11.1
version: 2.11.1(electron@37.2.1)
'@hono/node-server':
specifier: 1.15.0
version: 1.15.0(hono@4.8.4)
@ -105,7 +105,7 @@ importers:
version: 14.0.0
custom-electron-prompt:
specifier: 1.5.8
version: 1.5.8(electron@37.2.0)
version: 1.5.8(electron@37.2.1)
deepmerge-ts:
specifier: 7.1.5
version: 7.1.5
@ -255,8 +255,8 @@ importers:
specifier: 0.4.0
version: 0.4.0(patch_hash=c787371eeb2af011ea934e8818a0dad6d7dcb2df31bbb1686babc7231af0183c)
'@playwright/test':
specifier: 1.53.2
version: 1.53.2
specifier: 1.54.1
version: 1.54.1
'@stylistic/eslint-plugin':
specifier: 5.1.0
version: 5.1.0(eslint@9.30.1)
@ -294,8 +294,8 @@ importers:
specifier: 0.38.15
version: 0.38.15
electron:
specifier: 37.2.0
version: 37.2.0
specifier: 37.2.1
version: 37.2.1
electron-builder:
specifier: 26.0.12
version: 26.0.12(electron-builder-squirrel-windows@26.0.12)
@ -339,8 +339,8 @@ importers:
specifier: 11.2.0
version: 11.2.0
playwright:
specifier: 1.53.2
version: 1.53.2
specifier: 1.54.1
version: 1.54.1
rollup:
specifier: 4.44.2
version: 4.44.2
@ -783,24 +783,24 @@ packages:
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
'@ghostery/adblocker-content@2.11.0':
resolution: {integrity: sha512-2TIxD5OC9lQpCq1SNC9mu4M37pZf1+1LDJCd4yM8gTGc927xMwNVkUBNDmi1aEH2r+PMtmJu4xtHN+YFhgns6A==}
'@ghostery/adblocker-content@2.11.1':
resolution: {integrity: sha512-KF1vjwp/YNzgjLKOMh+FjGcXbliZHIJnh2A9+dLgL8Fe+HCK1h49p7xa7FFa0c2pO45GDfI2LgsAiqhHSYZP6A==}
'@ghostery/adblocker-electron-preload@2.11.0':
resolution: {integrity: sha512-sI3571exKBH1MYz74QEADKhJkqayWZoawIfjm3rBrFxtuppHOIEgiMgYbg+KxdprBohXkRLknrAUWMEj27oBZA==}
'@ghostery/adblocker-electron-preload@2.11.1':
resolution: {integrity: sha512-5bJFfvxml4g/hyfnG3cVm3Y85gdjscRhKA80OjtF2xmEBzEnQbM2rWUkq2dV1peURcPeK53D+jGhOjROGa3OHQ==}
peerDependencies:
electron: '>11'
'@ghostery/adblocker-electron@2.11.0':
resolution: {integrity: sha512-RMwwwWOrF2efl4u0ry1dJOFu5Hfl1Qgr5rEQKnZvG8U6my+C4HyQIsmbhr7qhviTGpPUYO16FoWrsX7rG05i5w==}
'@ghostery/adblocker-electron@2.11.1':
resolution: {integrity: sha512-Nr4jOBMZkCBciMRMrUtsHHFI3d4mGfcQJi1TYmhu8oPGnDTfSPnddFLRYwoefsnSReZJNy4IuHBYQjV2NUeB9A==}
peerDependencies:
electron: '>11'
'@ghostery/adblocker-extended-selectors@2.11.0':
resolution: {integrity: sha512-yl7Ygs+TFrG2ZbxynUG8Tyt3mI4WPb4Gzy4VgVXV29SYwUbok0md0Hp7vHSYeURlSzEMMa/KqgjFoZr3GY49XA==}
'@ghostery/adblocker-extended-selectors@2.11.1':
resolution: {integrity: sha512-T0UpheA3cd9T/4feJkJR+i5QaDvRt5gHcFWKh7DAstdYPqbKxqDG0U2RgY1Z8PtG2dVmIzBlQ0QPUB7gCF6nyw==}
'@ghostery/adblocker@2.11.0':
resolution: {integrity: sha512-7FW/Hd/lYp2OLDw6JknYLIhEoFTkcGTC3PqMu9JiOYV1CgUJ/PKhcsRsmdm1lAbhmL9Rq86z1/bhn8CZXSPZ/w==}
'@ghostery/adblocker@2.11.1':
resolution: {integrity: sha512-bROAa2LayP0CfY6dJodMkV/DN7/0OgSEOrj/TnTWrOAkiSrH4rGpO3G1NlIAyF4uK9IZNOWDzcZt7/vv9qy0zA==}
'@ghostery/url-parser@1.3.0':
resolution: {integrity: sha512-FEzdSeiva0Mt3bR4xePFzthhjT4IzvA5QTvS1xXkNyLpMGeq40mb3V2fSs0ZItRaP9IybZthDfHUSbQ1HLdx4Q==}
@ -1067,8 +1067,8 @@ packages:
resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@playwright/test@1.53.2':
resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==}
'@playwright/test@1.54.1':
resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==}
engines: {node: '>=18'}
hasBin: true
@ -2322,8 +2322,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
electron@37.2.0:
resolution: {integrity: sha512-dE6+qeg6SBUVd5d8CD2+GH82kh+gF1v40+hs+U+UOno681NMSGmBtgqwldQRpbvtnQDD7V2M9Cpfr3+Abw7aBg==}
electron@37.2.1:
resolution: {integrity: sha512-ae2EbzRNqIAHlftfCHtbbt6EgJUW8+zxWLONqNnn2iSrLF0O/pbxbff3xcpZYPpmFBs4uqjoi+s4QS7DQ+zZ/w==}
engines: {node: '>= 12.20.55'}
hasBin: true
@ -3908,13 +3908,13 @@ packages:
resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
hasBin: true
playwright-core@1.53.2:
resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==}
playwright-core@1.54.1:
resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==}
engines: {node: '>=18'}
hasBin: true
playwright@1.53.2:
resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==}
playwright@1.54.1:
resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==}
engines: {node: '>=18'}
hasBin: true
@ -5173,9 +5173,9 @@ snapshots:
- bluebird
- supports-color
'@electron/remote@2.1.3(electron@37.2.0)':
'@electron/remote@2.1.3(electron@37.2.1)':
dependencies:
electron: 37.2.0
electron: 37.2.1
'@electron/universal@3.0.0':
dependencies:
@ -5362,28 +5362,28 @@ snapshots:
'@gar/promisify@1.1.3': {}
'@ghostery/adblocker-content@2.11.0':
'@ghostery/adblocker-content@2.11.1':
dependencies:
'@ghostery/adblocker-extended-selectors': 2.11.0
'@ghostery/adblocker-extended-selectors': 2.11.1
'@ghostery/adblocker-electron-preload@2.11.0(electron@37.2.0)':
'@ghostery/adblocker-electron-preload@2.11.1(electron@37.2.1)':
dependencies:
'@ghostery/adblocker-content': 2.11.0
electron: 37.2.0
'@ghostery/adblocker-content': 2.11.1
electron: 37.2.1
'@ghostery/adblocker-electron@2.11.0(electron@37.2.0)':
'@ghostery/adblocker-electron@2.11.1(electron@37.2.1)':
dependencies:
'@ghostery/adblocker': 2.11.0
'@ghostery/adblocker-electron-preload': 2.11.0(electron@37.2.0)
electron: 37.2.0
'@ghostery/adblocker': 2.11.1
'@ghostery/adblocker-electron-preload': 2.11.1(electron@37.2.1)
electron: 37.2.1
tldts-experimental: 7.0.9
'@ghostery/adblocker-extended-selectors@2.11.0': {}
'@ghostery/adblocker-extended-selectors@2.11.1': {}
'@ghostery/adblocker@2.11.0':
'@ghostery/adblocker@2.11.1':
dependencies:
'@ghostery/adblocker-content': 2.11.0
'@ghostery/adblocker-extended-selectors': 2.11.0
'@ghostery/adblocker-content': 2.11.1
'@ghostery/adblocker-extended-selectors': 2.11.1
'@ghostery/url-parser': 1.3.0
'@remusao/guess-url-type': 2.1.0
'@remusao/small': 2.1.0
@ -5742,9 +5742,9 @@ snapshots:
'@pkgr/core@0.2.7': {}
'@playwright/test@1.53.2':
'@playwright/test@1.54.1':
dependencies:
playwright: 1.53.2
playwright: 1.54.1
'@polka/url@1.0.0-next.28': {}
@ -5960,7 +5960,7 @@ snapshots:
'@types/electron-localshortcut@3.1.3':
dependencies:
electron: 37.2.0
electron: 37.2.1
transitivePeerDependencies:
- supports-color
@ -6807,9 +6807,9 @@ snapshots:
csstype@3.1.3: {}
custom-electron-prompt@1.5.8(electron@37.2.0):
custom-electron-prompt@1.5.8(electron@37.2.1):
dependencies:
electron: 37.2.0
electron: 37.2.1
data-uri-to-buffer@4.0.1: {}
@ -7141,7 +7141,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@37.2.0:
electron@37.2.1:
dependencies:
'@electron/get': 2.0.3
'@types/node': 22.13.5
@ -8858,11 +8858,11 @@ snapshots:
dependencies:
pngjs: 6.0.0
playwright-core@1.53.2: {}
playwright-core@1.54.1: {}
playwright@1.53.2:
playwright@1.54.1:
dependencies:
playwright-core: 1.53.2
playwright-core: 1.54.1
optionalDependencies:
fsevents: 2.3.2

View File

@ -11,9 +11,9 @@ export function getPlugins() {
return store.get('plugins') as Record<string, PluginConfig>;
}
export function isEnabled(plugin: string) {
export async function isEnabled(plugin: string) {
const pluginConfig = deepmerge(
allPlugins[plugin].config ?? { enabled: false },
(await allPlugins())[plugin].config ?? { enabled: false },
(store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {},
);
return pluginConfig !== undefined && pluginConfig.enabled;

View File

@ -4,7 +4,7 @@ import { languageResources } from 'virtual:i18n';
export const loadI18n = async () =>
await init({
resources: languageResources,
resources: await languageResources(),
lng: 'en',
fallbackLng: 'en',
interpolation: {

View File

@ -62,10 +62,10 @@ import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config';
import type { PluginConfig } from '@/types/plugins';
if (!is.macOS()) {
delete allPlugins['touchbar'];
delete (await allPlugins())['touchbar'];
}
if (!is.windows()) {
delete allPlugins['taskbar-mediacontrol'];
delete (await allPlugins())['taskbar-mediacontrol'];
}
// Catch errors and log them
@ -139,13 +139,13 @@ if (is.linux()) {
app.setName('com.github.th_ch.youtube_music');
// Stops chromium from launching its own MPRIS service
if (config.plugins.isEnabled('shortcuts')) {
if (await config.plugins.isEnabled('shortcuts')) {
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
}
}
if (config.get('options.proxy')) {
const authProxyEnabled = config.plugins.isEnabled('auth-proxy-adapter');
const authProxyEnabled = await config.plugins.isEnabled('auth-proxy-adapter');
let proxyToUse = '';
if (authProxyEnabled) {
@ -183,19 +183,23 @@ function onClosed() {
mainWindow = null;
}
ipcMain.handle('ytmd:get-main-plugin-names', () => Object.keys(mainPlugins));
ipcMain.handle('ytmd:get-main-plugin-names', async () =>
Object.keys(await mainPlugins()),
);
const initHook = async (win: BrowserWindow) => {
const allPluginStubs = await allPlugins();
const initHook = (win: BrowserWindow) => {
ipcMain.handle(
'ytmd:get-config',
(_, id: string) =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
allPluginStubs[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
);
ipcMain.handle('ytmd:set-config', (_, name: string, obj: object) =>
config.setPartial(`plugins.${name}`, obj, allPlugins[name].config),
config.setPartial(`plugins.${name}`, obj, allPluginStubs[name].config),
);
config.watch((newValue, oldValue) => {
@ -214,7 +218,7 @@ const initHook = (win: BrowserWindow) => {
if (!isEqual) {
const oldConfig = oldPluginConfigList[id] as PluginConfig;
const config = deepmerge(
allPlugins[id].config ?? { enabled: false },
allPluginStubs[id].config ?? { enabled: false },
newPluginConfig ?? {},
) as PluginConfig;
@ -229,7 +233,7 @@ const initHook = (win: BrowserWindow) => {
forceUnloadMainPlugin(id, win);
}
if (allPlugins[id]?.restartNeeded) {
if (allPluginStubs[id]?.restartNeeded) {
showNeedToRestartDialog(id);
}
}
@ -250,8 +254,8 @@ const initHook = (win: BrowserWindow) => {
});
};
const showNeedToRestartDialog = (id: string) => {
const plugin = allPlugins[id];
const showNeedToRestartDialog = async (id: string) => {
const plugin = (await allPlugins())[id];
const dialogOptions: Electron.MessageBoxOptions = {
type: 'info',
@ -325,7 +329,7 @@ async function createMainWindow() {
const windowSize = config.get('window-size');
const windowMaximized = config.get('window-maximized');
const windowPosition: Electron.Point = config.get('window-position');
const useInlineMenu = config.plugins.isEnabled('in-app-menu');
const useInlineMenu = await config.plugins.isEnabled('in-app-menu');
const defaultTitleBarOverlayOptions: Electron.TitleBarOverlay = {
color: '#00000000',
@ -369,7 +373,7 @@ async function createMainWindow() {
},
...decorations,
});
initHook(win);
await initHook(win);
initTheme(win);
await loadAllMainPlugins(win);
@ -614,12 +618,12 @@ app.on('activate', async () => {
}
});
const getDefaultLocale = (locale: string) =>
Object.keys(languageResources).includes(locale) ? locale : null;
const getDefaultLocale = async (locale: string) =>
Object.keys(await languageResources()).includes(locale) ? locale : null;
app.whenReady().then(async () => {
if (!config.get('options.language')) {
const locale = getDefaultLocale(app.getLocale());
const locale = await getDefaultLocale(app.getLocale());
if (locale) {
config.set('options.language', locale);
}

View File

@ -20,13 +20,17 @@ const createContext = (
id: string,
win: BrowserWindow,
): BackendContext<PluginConfig> => ({
getConfig: () =>
getConfig: async () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
(await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
setConfig: async (newConfig) => {
config.setPartial(
`plugins.${id}`,
newConfig,
(await allPlugins())[id].config,
);
},
ipc: {
@ -96,7 +100,7 @@ export const forceLoadMainPlugin = async (
id: string,
win: BrowserWindow,
): Promise<void> => {
const plugin = mainPlugins[id];
const plugin = (await mainPlugins())[id];
if (!plugin) return;
try {
@ -133,7 +137,7 @@ export const loadAllMainPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins();
const queue: Promise<void>[] = [];
for (const [plugin, pluginDef] of Object.entries(mainPlugins)) {
for (const [plugin, pluginDef] of Object.entries(await mainPlugins())) {
const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {});
if (config.enabled) {
queue.push(forceLoadMainPlugin(plugin, win));

View File

@ -17,19 +17,23 @@ const createContext = (
id: string,
win: BrowserWindow,
): MenuContext<PluginConfig> => ({
getConfig: () =>
getConfig: async () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
(await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
setConfig: async (newConfig) => {
config.setPartial(
`plugins.${id}`,
newConfig,
(await allPlugins())[id].config,
);
},
window: win,
refresh: async () => {
await setApplicationMenu(win);
if (config.plugins.isEnabled('in-app-menu')) {
if (await config.plugins.isEnabled('in-app-menu')) {
win.webContents.send('refresh-in-app-menu');
}
},
@ -37,7 +41,7 @@ const createContext = (
export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
try {
const plugin = allPlugins[id];
const plugin = (await allPlugins())[id];
if (!plugin) return;
const menu = plugin.menu?.(createContext(id, win));
@ -68,7 +72,7 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
export const loadAllMenuPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(allPlugins)) {
for (const [pluginId, pluginDef] of Object.entries(await allPlugins())) {
const config = deepmerge(
pluginDef.config ?? { enabled: false },
pluginConfigs[pluginId] ?? {},

View File

@ -15,13 +15,17 @@ const loadedPluginMap: Record<
PluginDef<unknown, unknown, unknown>
> = {};
const createContext = (id: string): PreloadContext<PluginConfig> => ({
getConfig: () =>
getConfig: async () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
(await allPlugins())[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
setConfig: async (newConfig) => {
config.setPartial(
`plugins.${id}`,
newConfig,
(await allPlugins())[id].config,
);
},
});
@ -48,7 +52,7 @@ export const forceUnloadPreloadPlugin = async (id: string) => {
export const forceLoadPreloadPlugin = async (id: string) => {
try {
const plugin = preloadPlugins[id];
const plugin = (await preloadPlugins())[id];
if (!plugin) return;
const hasStarted = await startPlugin(id, plugin, {
@ -78,10 +82,10 @@ export const forceLoadPreloadPlugin = async (id: string) => {
}
};
export const loadAllPreloadPlugins = () => {
export const loadAllPreloadPlugins = async () => {
const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) {
for (const [pluginId, pluginDef] of Object.entries(await preloadPlugins())) {
const config = deepmerge(
pluginDef.config ?? { enable: false },
pluginConfigs[pluginId] ?? {},

View File

@ -18,7 +18,7 @@ const loadedPluginMap: Record<
export const createContext = <Config extends PluginConfig>(
id: string,
): RendererContext<Config> => ({
getConfig: async () =>
getConfig: () =>
window.ipcRenderer.invoke('ytmd:get-config', id) as Promise<Config>,
setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
@ -47,7 +47,7 @@ export const forceUnloadRendererPlugin = async (id: string) => {
delete unregisterStyleMap[id];
delete loadedPluginMap[id];
const plugin = rendererPlugins[id];
const plugin = (await rendererPlugins())[id];
if (!plugin) return;
const hasStopped = await stopPlugin(id, plugin, {
@ -71,7 +71,7 @@ export const forceUnloadRendererPlugin = async (id: string) => {
};
export const forceLoadRendererPlugin = async (id: string) => {
const plugin = rendererPlugins[id];
const plugin = (await rendererPlugins())[id];
if (!plugin) return;
const hasEvaled = await startPlugin(id, plugin, {
@ -117,7 +117,7 @@ export const forceLoadRendererPlugin = async (id: string) => {
export const loadAllRendererPlugins = async () => {
const pluginConfigs = window.mainConfig.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) {
for (const [pluginId, pluginDef] of Object.entries(await rendererPlugins())) {
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
if (config.enabled) {

View File

@ -29,21 +29,21 @@ import packageJson from '../package.json';
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
// True only if in-app-menu was loaded on launch
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
const inAppMenuActive = await config.plugins.isEnabled('in-app-menu');
const pluginEnabledMenu = (
const pluginEnabledMenu = async (
plugin: string,
label = '',
description: string | undefined = undefined,
isNew = false,
hasSubmenu = false,
refreshMenu: (() => void) | undefined = undefined,
): Electron.MenuItemConstructorOptions => ({
): Promise<Electron.MenuItemConstructorOptions> => ({
label: label || plugin,
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: description,
type: 'checkbox',
checked: config.plugins.isEnabled(plugin),
checked: await config.plugins.isEnabled(plugin),
click(item: Electron.MenuItem) {
if (item.checked) {
config.plugins.enable(plugin);
@ -71,19 +71,21 @@ export const mainMenuTemplate = async (
const { navigationHistory } = win.webContents;
await loadAllMenuPlugins(win);
const menuResult = Object.entries(getAllMenuTemplate()).map(
([id, template]) => {
const plugin = allPlugins[id];
const allPluginsStubs = await allPlugins();
const menuResult = await Promise.all(
Object.entries(getAllMenuTemplate()).map(async ([id, template]) => {
const plugin = allPluginsStubs[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
if (!config.plugins.isEnabled(id)) {
if (!(await config.plugins.isEnabled(id))) {
return [
id,
pluginEnabledMenu(
await pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
@ -101,7 +103,7 @@ export const mainMenuTemplate = async (
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: pluginDescription,
submenu: [
pluginEnabledMenu(
await pluginEnabledMenu(
id,
t('main.menu.plugins.enabled'),
undefined,
@ -114,39 +116,42 @@ export const mainMenuTemplate = async (
],
} satisfies Electron.MenuItemConstructorOptions,
] as const;
},
}),
);
const availablePlugins = Object.keys(allPlugins);
const pluginMenus = availablePlugins
.sort((a, b) => {
const aPluginLabel = allPlugins[a]?.name?.() ?? a;
const bPluginLabel = allPlugins[b]?.name?.() ?? b;
const availablePlugins = Object.keys(await allPlugins());
const pluginMenus = await Promise.all(
availablePlugins
.sort((a, b) => {
const aPluginLabel = allPluginsStubs[a]?.name?.() ?? a;
const bPluginLabel = allPluginsStubs[b]?.name?.() ?? b;
return aPluginLabel.localeCompare(bPluginLabel);
})
.map((id) => {
const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1];
return aPluginLabel.localeCompare(bPluginLabel);
})
.map((id) => {
const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1];
const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
const plugin = allPluginsStubs[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
return pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
isNew,
true,
innerRefreshMenu,
);
});
return pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
isNew,
true,
innerRefreshMenu,
);
}),
);
const availableLanguages = Object.keys(languageResources);
const langResources = await languageResources();
const availableLanguages = Object.keys(langResources);
return [
{
@ -445,7 +450,7 @@ export const mainMenuTemplate = async (
availableLanguages
.map(
(lang): Electron.MenuItemConstructorOptions => ({
label: `${languageResources[lang].translation.language?.name ?? 'Unknown'} (${languageResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`,
label: `${langResources[lang].translation.language?.name ?? 'Unknown'} (${langResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`,
type: 'checkbox',
checked: (config.get('options.language') ?? 'en') === lang,
click() {

View File

@ -13,7 +13,6 @@ import { DownloadButton } from './templates/download';
import type { RendererContext } from '@/types/contexts';
import type { DownloaderPluginConfig } from './index';
let menu: HTMLElement | null = null;
let download: () => void;
const [downloadButtonText, setDownloadButtonText] = createSignal<string>('');
@ -21,14 +20,10 @@ const [downloadButtonText, setDownloadButtonText] = createSignal<string>('');
let buttonContainer: HTMLDivElement | null = null;
const menuObserver = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
if (!menu) {
return;
}
}
const menu = getSongMenu();
if (
!menu ||
menu.contains(buttonContainer) ||
!isMusicOrVideoTrack() ||
!buttonContainer

View File

@ -3,7 +3,10 @@ import keyEventAreEqual from 'keyboardevents-areequal';
import { render } from 'solid-js/web';
import { getSongMenu } from '@/providers/dom-elements';
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
import {
isMusicOrVideoTrack,
isPlayerMenu,
} from '@/plugins/utils/renderer/check';
import { t } from '@/i18n';
@ -152,7 +155,11 @@ export const onPlayerApiReady = async (
const observer = new MutationObserver(() => {
const menu = getSongMenu();
if (menu?.contains(pipButtonContainer) || !isMusicOrVideoTrack()) {
if (
menu?.contains(pipButtonContainer) ||
!isMusicOrVideoTrack() ||
!isPlayerMenu(menu)
) {
return;
}

View File

@ -7,7 +7,10 @@ import { getSongMenu } from '@/providers/dom-elements';
import { PlaybackSpeedSlider } from './components/slider';
import { t } from '@/i18n';
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
import {
isMusicOrVideoTrack,
isPlayerMenu,
} from '@/plugins/utils/renderer/check';
const MIN_PLAYBACK_SPEED = 0.07;
const MAX_PLAYBACK_SPEED = 16;
@ -83,7 +86,12 @@ export const onPlayerApiReady = () => {
const observer = new MutationObserver(() => {
const menu = getSongMenu();
if (menu && !menu.contains(sliderContainer) && isMusicOrVideoTrack()) {
if (
menu &&
!menu.contains(sliderContainer) &&
isMusicOrVideoTrack() &&
isPlayerMenu(menu)
) {
menu.prepend(sliderContainer);
}
});

View File

@ -45,7 +45,7 @@ export const onPlayerApiReady = async (
}, 2500);
/** Restore saved volume and setup tooltip */
function firstRun() {
async function firstRun() {
if (typeof options.savedVolume === 'number') {
// Set saved volume as tooltip
setTooltip(options.savedVolume);
@ -66,7 +66,7 @@ export const onPlayerApiReady = async (
injectVolumeHud(noVid);
if (!noVid) {
setupVideoPlayerOnwheel();
if (!window.mainConfig.plugins.isEnabled('video-toggle')) {
if (!await window.mainConfig.plugins.isEnabled('video-toggle')) {
// Video-toggle handles hud positioning on its own
const videoMode = () =>
api.getPlayerResponse().videoDetails?.musicVideoType !==
@ -280,7 +280,7 @@ export const onPlayerApiReady = async (
);
context.ipc.on('setVolume', (value: number) => setVolume(value));
firstRun();
await firstRun();
};
export const onConfigChange = (config: PreciseVolumePluginConfig) => {

View File

@ -309,8 +309,8 @@ function registerMPRIS(win: BrowserWindow) {
player.volume = Number.parseFloat((newVol / 100).toFixed(2));
});
player.on('volume', (newVolume: number) => {
if (config.plugins.isEnabled('precise-volume')) {
player.on('volume', async (newVolume: number) => {
if (await config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value.
win.webContents.send('setVolume', ~~(newVolume * 100));
} else {

View File

@ -1,20 +1,39 @@
export const isMusicOrVideoTrack = () => {
let menuUrl = document.querySelector<HTMLAnchorElement>(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href;
if (!menuUrl?.includes('watch?')) {
menuUrl = undefined;
// check for podcast
for (const it of document.querySelectorAll(
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
)) {
if (it.getAttribute('href')?.includes('podcast/')) {
menuUrl = it.getAttribute('href')!;
break;
}
for (const menuSelector of document.querySelectorAll<
HTMLAnchorElement & {
data: {
watchEndpoint: {
videoId: string;
};
addToPlaylistEndpoint: {
videoId: string;
};
clickTrackingParams: string;
};
}
>('tp-yt-paper-listbox #navigation-endpoint')) {
if (
menuSelector?.data?.addToPlaylistEndpoint?.videoId ||
menuSelector?.data?.watchEndpoint?.videoId
) {
return true;
}
}
return !!menuUrl;
return false;
};
export const isPlayerMenu = (menu?: HTMLElement | null) => {
return (
menu?.parentElement as
| (HTMLElement & {
ytEventForwardingBehavior: {
forwarder_: {
eventSink: HTMLElement;
};
};
})
| null
)?.ytEventForwardingBehavior?.forwarder_?.eventSink?.matches(
'ytmusic-menu-renderer.ytmusic-player-bar',
);
};

View File

@ -159,9 +159,9 @@ export default createPlugin({
const config = await getConfig();
this.config = config;
const moveVolumeHud = window.mainConfig.plugins.isEnabled(
const moveVolumeHud = (await window.mainConfig.plugins.isEnabled(
'precise-volume',
)
))
? (preciseVolumeMoveVolumeHud as (_: boolean) => void)
: () => {};

View File

@ -17,7 +17,7 @@ import { loadI18n, setLanguage } from '@/i18n';
loadI18n().then(async () => {
await setLanguage(config.get('options.language') ?? 'en');
loadAllPreloadPlugins();
await loadAllPreloadPlugins();
});
ipcRenderer.on('plugin:unload', async (_, id: string) => {

View File

@ -3,18 +3,17 @@ declare module 'virtual:plugins' {
type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>;
export const mainPlugins: Record<string, Plugin>;
export const preloadPlugins: Record<string, Plugin>;
export const rendererPlugins: Record<string, Plugin>;
export const mainPlugins: () => Promise<Record<string, Plugin>>;
export const preloadPlugins: () => Promise<Record<string, Plugin>>;
export const rendererPlugins: () => Promise<Record<string, Plugin>>;
export const allPlugins: Record<
string,
Omit<Plugin, 'backend' | 'preload' | 'renderer'>
export const allPlugins: () => Promise<
Record<string, Omit<Plugin, 'backend' | 'preload' | 'renderer'>>
>;
}
declare module 'virtual:i18n' {
import type { LanguageResources } from '@/i18n/resources/@types';
export const languageResources: LanguageResources;
export const languageResources: () => Promise<LanguageResources>;
}

View File

@ -4,9 +4,6 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { Project } from 'ts-morph';
const snakeToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
const __dirname = dirname(fileURLToPath(import.meta.url));
const globalProject = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
@ -27,20 +24,20 @@ export const i18nImporter = () => {
const src = globalProject.createSourceFile(
'vm:i18n',
(writer) => {
// prettier-ignore
writer.writeLine('export const languageResources = async () => {');
writer.writeLine(' const entries = await Promise.all([');
for (const { name, path } of plugins) {
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`);
}
const relativePath = relative(resolve(srcPath, '..'), path).replace(
/\\/g,
'/',
);
writer.blankLine();
writer.writeLine('export const languageResources = {');
for (const { name } of plugins) {
writer.writeLine(` "${name}": {`);
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
writer.writeLine(' },');
writer.writeLine(
` import('./${relativePath}').then((mod) => ({ "${name}": { translation: mod.default } })),`,
);
}
writer.writeLine(' ]);');
writer.writeLine(' return Object.assign({}, ...entries);');
writer.writeLine('};');
writer.blankLine();
},

View File

@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { Project } from 'ts-morph';
const snakeToCamel = (text: string) =>
const kebabToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -43,31 +43,83 @@ export const pluginVirtualModuleGenerator = (
const src = globalProject.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, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
}
const relativePath = relative(resolve(srcPath, '..'), path).replace(
/\\/g,
'/',
);
if (mode === 'main') {
// dynamic import (for main)
writer.writeLine(
`const ${kebabToCamel(name)}PluginImport = () => import('./${relativePath}');`,
);
writer.writeLine(
`const ${kebabToCamel(name)}Plugin = async () => (await ${kebabToCamel(name)}PluginImport()).default;`,
);
writer.writeLine(
`const ${kebabToCamel(name)}PluginStub = async () => (await ${kebabToCamel(name)}PluginImport()).pluginStub;`,
);
} else {
// static import (preload does not support dynamic import)
writer.writeLine(
`import ${kebabToCamel(name)}PluginImport, { pluginStub as ${kebabToCamel(name)}PluginStubImport } from "./${relativePath}";`,
);
writer.writeLine(
`const ${kebabToCamel(name)}Plugin = () => Promise.resolve(${kebabToCamel(name)}PluginImport);`,
);
writer.writeLine(
`const ${kebabToCamel(name)}PluginStub = () => Promise.resolve(${kebabToCamel(name)}PluginStubImport);`,
);
}
}
writer.blankLine();
// Context-specific exports
writer.writeLine(`export const ${mode}Plugins = {`);
writer.writeLine(`let ${mode}PluginsCache = null;`);
writer.writeLine(`export const ${mode}Plugins = async () => {`);
writer.writeLine(
` if (${mode}PluginsCache) return await ${mode}PluginsCache;`,
);
writer.writeLine(
' const { promise, resolve } = Promise.withResolvers();',
);
writer.writeLine(' ' + `${mode}PluginsCache = promise;`);
writer.writeLine(' const pluginEntries = await Promise.all([');
for (const { name } of plugins) {
const checkMode = mode === 'main' ? 'backend' : mode;
// HACK: To avoid situation like importing renderer plugins in main
writer.writeLine(
` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`,
` ${kebabToCamel(name)}Plugin().then((plg) => plg['${checkMode}'] ? ["${name}", plg] : null),`,
);
}
writer.writeLine(' ]);');
writer.writeLine(
' resolve(pluginEntries.filter((entry) => entry).reduce((acc, [name, plg]) => { acc[name] = plg; return acc; }, {}));',
);
writer.writeLine(` return await ${mode}PluginsCache;`);
writer.writeLine('};');
writer.blankLine();
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
writer.writeLine('export const allPlugins = {');
writer.writeLine('let allPluginsCache = null;');
writer.writeLine('export const allPlugins = async () => {');
writer.writeLine(' if (allPluginsCache) return await allPluginsCache;');
writer.writeLine(
' const { promise, resolve } = Promise.withResolvers();',
);
writer.writeLine(' allPluginsCache = promise;');
writer.writeLine(' const stubEntries = await Promise.all([');
for (const { name } of plugins) {
writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
writer.writeLine(
` ${kebabToCamel(name)}PluginStub().then((stub) => ["${name}", stub]),`,
);
}
writer.writeLine(' ]);');
writer.writeLine(
' resolve(stubEntries.reduce((acc, [name, plg]) => { acc[name] = plg; return acc; }, {}));',
);
writer.writeLine(' return await promise;');
writer.writeLine('};');
writer.blankLine();
},