refactor: remove dynamic require (partial of #2)

Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2023-10-03 23:42:12 +09:00
parent 6eadc7f7e5
commit 6e315b9af2
24 changed files with 841 additions and 745 deletions

View File

@ -67,11 +67,8 @@ const defaultConfig = {
overrideUserAgent: false, overrideUserAgent: false,
themes: [] as string[], themes: [] as string[],
}, },
/** please order alphabetically */
'plugins': { 'plugins': {
// Enabled plugins
'navigation': {
enabled: true,
},
'adblocker': { 'adblocker': {
enabled: true, enabled: true,
cache: true, cache: true,
@ -79,7 +76,95 @@ const defaultConfig = {
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt" additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
disableDefaultLists: [], disableDefaultLists: [],
}, },
// Disabled plugins 'album-color-theme': {},
'audio-compressor': {},
'blur-nav-bar': {},
'bypass-age-restrictions': {},
'captions-selector': {
enabled: false,
disableCaptions: false,
autoload: false,
lastCaptionsCode: '',
disabledCaptions: false,
},
'compact-sidebar': {},
'crossfade': {
enabled: false,
fadeInDuration: 1500, // Ms
fadeOutDuration: 5000, // Ms
secondsBeforeEnd: 10, // S
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
},
'disable-autoplay': {
applyOnce: false,
},
'discord': {
enabled: false,
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
listenAlong: true, // Add a "listen along" button to rich presence
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
},
'downloader': {
enabled: false,
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
preset: 'mp3',
skipExisting: false,
playlistMaxItems: undefined as number | undefined,
},
'exponential-volume': {},
'in-app-menu': {},
'last-fm': {
enabled: false,
token: undefined as string | undefined, // Token used for authentication
session_key: undefined as string | undefined, // Session key used for scrobbling
api_root: 'http://ws.audioscrobbler.com/2.0/',
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
secret: 'a5d2a36fdf64819290f6982481eaffa2',
},
'lyrics-genius': {
romanizedLyrics: false,
},
'navigation': {
enabled: true,
},
'no-google-login': {},
'notifications': {
enabled: false,
unpauseNotification: false,
urgency: 'normal', // Has effect only on Linux
// the following has effect only on Windows
interactive: true,
toastStyle: 1, // See plugins/notifications/utils for more info
refreshOnPlayPause: false,
trayControls: true,
hideButtonText: false,
},
'picture-in-picture': {
'enabled': false,
'alwaysOnTop': true,
'savePosition': true,
'saveSize': false,
'hotkey': 'P',
'pip-position': [10, 10],
'pip-size': [450, 275],
'isInPiP': false,
'useNativePiP': false,
},
'playback-speed': {},
'precise-volume': {
enabled: false,
steps: 1, // Percentage of volume to change
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
volumeUp: '',
volumeDown: '',
},
savedVolume: undefined as number | undefined, // Plugin save volume between session here
},
'quality-changer': {},
'shortcuts': { 'shortcuts': {
enabled: false, enabled: false,
overrideMediaKeys: false, overrideMediaKeys: false,
@ -94,56 +179,8 @@ const defaultConfig = {
next: '', next: '',
} as Record<string, string>, } as Record<string, string>,
}, },
'downloader': { 'skip-silences': {
enabled: false, onlySkipBeginning: false,
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
preset: 'mp3',
skipExisting: false,
playlistMaxItems: undefined as number | undefined,
},
'last-fm': {
enabled: false,
token: undefined as string | undefined, // Token used for authentication
session_key: undefined as string | undefined, // Session key used for scrobbling
api_root: 'http://ws.audioscrobbler.com/2.0/',
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
secret: 'a5d2a36fdf64819290f6982481eaffa2',
},
'lyric-genius': {
romanizedLyrics: false,
},
'disable-autoplay': {
applyOnce: false,
},
'discord': {
enabled: false,
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
listenAlong: true, // Add a "listen along" button to rich presence
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
},
'notifications': {
enabled: false,
unpauseNotification: false,
urgency: 'normal', // Has effect only on Linux
// the following has effect only on Windows
interactive: true,
toastStyle: 1, // See plugins/notifications/utils for more info
refreshOnPlayPause: false,
trayControls: true,
hideButtonText: false,
},
'precise-volume': {
enabled: false,
steps: 1, // Percentage of volume to change
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
volumeUp: '',
volumeDown: '',
},
savedVolume: undefined as number | undefined, // Plugin save volume between session here
}, },
'sponsorblock': { 'sponsorblock': {
enabled: false, enabled: false,
@ -157,6 +194,9 @@ const defaultConfig = {
'music_offtopic', 'music_offtopic',
], ],
}, },
'taskbar-mediacontrol': {},
'touchbar': {},
'tuna-obs': {},
'video-toggle': { 'video-toggle': {
enabled: false, enabled: false,
hideVideo: false, hideVideo: false,
@ -164,34 +204,6 @@ const defaultConfig = {
forceHide: false, forceHide: false,
align: '', align: '',
}, },
'picture-in-picture': {
'enabled': false,
'alwaysOnTop': true,
'savePosition': true,
'saveSize': false,
'hotkey': 'P',
'pip-position': [10, 10],
'pip-size': [450, 275],
'isInPiP': false,
'useNativePiP': false,
},
'captions-selector': {
enabled: false,
disableCaptions: false,
autoload: false,
lastCaptionsCode: '',
disabledCaptions: false,
},
'skip-silences': {
onlySkipBeginning: false,
},
'crossfade': {
enabled: false,
fadeInDuration: 1500, // Ms
fadeOutDuration: 5000, // Ms
secondsBeforeEnd: 10, // S
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
},
'visualizer': { 'visualizer': {
enabled: false, enabled: false,
type: 'butterchurn', type: 'butterchurn',

View File

@ -10,9 +10,9 @@ import { getOptions, setMenuOptions, setOptions } from './plugins';
import { sendToFront } from '../providers/app-controls'; import { sendToFront } from '../providers/app-controls';
import { Entries } from '../utils/type-utils'; import { Entries } from '../utils/type-utils';
type DefaultPluginsConfig = typeof defaultConfig.plugins; export type DefaultPluginsConfig = typeof defaultConfig.plugins;
type OneOfDefaultConfigKey = keyof DefaultPluginsConfig; export type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey]; export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {}; const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {};

View File

@ -1,6 +1,6 @@
import path from 'node:path'; import path from 'node:path';
import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog } from 'electron'; import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
import enhanceWebRequest from 'electron-better-web-request'; import enhanceWebRequest from 'electron-better-web-request';
import is from 'electron-is'; import is from 'electron-is';
import unhandled from 'electron-unhandled'; import unhandled from 'electron-unhandled';
@ -18,6 +18,29 @@ 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 adblocker from './plugins/adblocker/back';
import albumColorTheme from './plugins/album-color-theme/back';
import blurNavigationBar from './plugins/blur-nav-bar/back';
import captionsSelector from './plugins/captions-selector/back';
import crossfade from './plugins/crossfade/back';
import discord from './plugins/discord/back';
import downloader from './plugins/downloader/back';
import inAppMenu from './plugins/in-app-menu/back';
import lastFm from './plugins/last-fm/back';
import lyricsGenius from './plugins/lyrics-genius/back';
import navigation from './plugins/navigation/back';
import noGoogleLogin from './plugins/no-google-login/back';
import notifications from './plugins/notifications/back';
import pictureInPicture, { setOptions as pipSetOptions } from './plugins/picture-in-picture/back';
import preciseVolume from './plugins/precise-volume/back';
import qualityChanger from './plugins/quality-changer/back';
import shortcuts from './plugins/shortcuts/back';
import sponsorBlock from './plugins/sponsorblock/back';
import taskbarMediaControl from './plugins/taskbar-mediacontrol/back';
import touchbar from './plugins/touchbar/back';
import tunaObs from './plugins/tuna-obs/back';
import videoToggle from './plugins/video-toggle/back';
import visualizer from './plugins/visualizer/back';
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
@ -75,6 +98,46 @@ function onClosed() {
mainWindow = null; mainWindow = null;
} }
const mainPlugins = {
'adblocker': adblocker,
'album-color-theme': albumColorTheme,
'blur-nav-bar': blurNavigationBar,
'captions-selector': captionsSelector,
'crossfade': crossfade,
'discord': discord,
'downloader': downloader,
'in-app-menu': inAppMenu,
'last-fm': lastFm,
'lyrics-genius': lyricsGenius,
'navigation': navigation,
'no-google-login': noGoogleLogin,
'notifications': notifications,
'picture-in-picture': pictureInPicture,
'precise-volume': preciseVolume,
'quality-changer': qualityChanger,
'shortcuts': shortcuts,
'sponsorblock': sponsorBlock,
'taskbar-mediacontrol': undefined as typeof taskbarMediaControl | undefined,
'touchbar': undefined as typeof touchbar | undefined,
'tuna-obs': tunaObs,
'video-toggle': videoToggle,
'visualizer': visualizer,
};
export const mainPluginNames = Object.keys(mainPlugins);
if (is.windows()) {
mainPlugins['taskbar-mediacontrol'] = taskbarMediaControl;
delete mainPlugins['touchbar'];
} else if (is.macOS()) {
mainPlugins['touchbar'] = touchbar;
delete mainPlugins['taskbar-mediacontrol'];
} else {
delete mainPlugins['touchbar'];
delete mainPlugins['taskbar-mediacontrol'];
}
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
function loadPlugins(win: BrowserWindow) { function loadPlugins(win: BrowserWindow) {
injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css')); injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
// Load user CSS // Load user CSS
@ -101,13 +164,17 @@ function loadPlugins(win: BrowserWindow) {
}); });
for (const [plugin, options] of config.plugins.getEnabled()) { for (const [plugin, options] of config.plugins.getEnabled()) {
try {
if (Object.hasOwn(mainPlugins, plugin)) {
console.log('Loaded plugin - ' + plugin); console.log('Loaded plugin - ' + plugin);
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js'); const handler = mainPlugins[plugin as keyof typeof mainPlugins];
fileExists(pluginPath, () => { if (handler) {
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access handler(win, options as never);
const handle = require(pluginPath).default as (window: BrowserWindow, option: typeof options) => void; }
handle(win, options); }
}); } catch (e) {
console.error(`Failed to load plugin "${plugin}"`, e);
}
} }
} }
@ -191,8 +258,7 @@ function createMainWindow() {
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture']; type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
const setPiPOptions = config.plugins.isEnabled('picture-in-picture') const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
? (key: string, value: unknown) => (require('./plugins/picture-in-picture/back') as typeof import('./plugins/picture-in-picture/back')) ? (key: string, value: unknown) => pipSetOptions({ [key]: value })
.setOptions({ [key]: value })
: () => {}; : () => {};
win.on('move', () => { win.on('move', () => {

56
menu.ts
View File

@ -1,21 +1,44 @@
import { existsSync } from 'node:fs';
import path from 'node:path';
import is from 'electron-is'; import is from 'electron-is';
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron'; import { app, BrowserWindow, clipboard, dialog, Menu } 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';
import { getAllPlugins } from './plugins/utils';
import config from './config'; import config from './config';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import promptOptions from './providers/prompt-options'; import promptOptions from './providers/prompt-options';
export type MenuTemplate = (Electron.MenuItemConstructorOptions | Electron.MenuItem)[]; import adblockerMenu from './plugins/adblocker/menu';
import captionsSelectorMenu from './plugins/captions-selector/menu';
import crossfadeMenu from './plugins/crossfade/menu';
import discordMenu from './plugins/discord/menu';
import downloaderMenu from './plugins/downloader/menu';
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
import notificationsMenu from './plugins/notifications/menu';
import preciseVolumeMenu from './plugins/precise-volume/menu';
import shortcutsMenu from './plugins/shortcuts/menu';
import videoToggleMenu from './plugins/video-toggle/menu';
import visualizerMenu from './plugins/visualizer/menu';
import { getAvailablePluginNames } from './plugins/utils';
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 pluginMenus = {
'adblocker': adblockerMenu,
'captions-selector': captionsSelectorMenu,
'crossfade': crossfadeMenu,
'discord': discordMenu,
'downloader': downloaderMenu,
'lyrics-genius': lyricsGeniusMenu,
'notifications': notificationsMenu,
'precise-volume': preciseVolumeMenu,
'shortcuts': shortcutsMenu,
'video-toggle': videoToggleMenu,
'visualizer': visualizerMenu,
};
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',
@ -45,33 +68,30 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
{ {
label: 'Plugins', label: 'Plugins',
submenu: submenu:
getAllPlugins().map((plugin) => { getAvailablePluginNames().map((pluginName) => {
const pluginPath = path.join(__dirname, 'plugins', plugin, 'menu.js'); if (Object.hasOwn(pluginMenus, pluginName)) {
if (existsSync(pluginPath)) { const getPluginMenu = pluginMenus[pluginName as keyof typeof pluginMenus];
let pluginLabel = plugin;
let pluginLabel = pluginName;
if (pluginLabel === 'crossfade') { if (pluginLabel === 'crossfade') {
pluginLabel = 'crossfade [beta]'; pluginLabel = 'crossfade [beta]';
} }
if (!config.plugins.isEnabled(plugin)) { if (!config.plugins.isEnabled(pluginName)) {
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu); return pluginEnabledMenu(pluginName, pluginLabel, true, refreshMenu);
} }
type PluginType = (window: BrowserWindow, plugins: string, func: () => void) => Electron.MenuItemConstructorOptions[];
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
const getPluginMenu = require(pluginPath).default as PluginType;
return { return {
label: pluginLabel, label: pluginLabel,
submenu: [ submenu: [
pluginEnabledMenu(plugin, 'Enabled', true, refreshMenu), pluginEnabledMenu(pluginName, 'Enabled', true, refreshMenu),
{ type: 'separator' }, { type: 'separator' },
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu), ...getPluginMenu(win, config.plugins.getOptions(pluginName), refreshMenu),
], ],
} satisfies Electron.MenuItemConstructorOptions; } satisfies Electron.MenuItemConstructorOptions;
} }
return pluginEnabledMenu(plugin); return pluginEnabledMenu(pluginName);
}), }),
}, },
{ {

View File

@ -0,0 +1,3 @@
export default () => {
require('@cliqz/adblocker-electron-preload');
};

3
plugins/adblocker/inject.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
const inject: () => void;
export default inject;

View File

@ -7,8 +7,8 @@
Parts of this code is derived from set-constant.js: Parts of this code is derived from set-constant.js:
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704 https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
*/ */
module.exports = () => {
{ {
const pruner = function (o) { const pruner = function (o) {
delete o.playerAds; delete o.playerAds;
delete o.adPlacements; delete o.adPlacements;
@ -33,9 +33,9 @@
return Reflect.apply(...arguments).then((o) => pruner(o)); return Reflect.apply(...arguments).then((o) => pruner(o));
}, },
}); });
} }
(function () { (function () {
let cValue = 'undefined'; let cValue = 'undefined';
const chain = 'playerResponse.adPlacements'; const chain = 'playerResponse.adPlacements';
const thisScript = document.currentScript; const thisScript = document.currentScript;
@ -232,9 +232,9 @@
// //
trapChain(window, chain); trapChain(window, chain);
})(); })();
(function () { (function () {
let cValue = 'undefined'; let cValue = 'undefined';
const thisScript = document.currentScript; const thisScript = document.currentScript;
const chain = 'ytInitialPlayerResponse.adPlacements'; const chain = 'ytInitialPlayerResponse.adPlacements';
@ -431,4 +431,5 @@
// //
trapChain(window, chain); trapChain(window, chain);
})(); })();
};

View File

@ -1,6 +1,8 @@
import config, { blockers } from './config'; import config, { blockers } from './config';
export default () => { import { MenuTemplate } from '../../menu';
export default (): MenuTemplate => {
return [ return [
{ {
label: 'Blocker', label: 'Blocker',

View File

@ -1,11 +1,13 @@
import config from './config'; import config from './config';
import inject from './inject';
import injectCliqzPreload from './inject-cliqz-preload';
export default async () => { export default async () => {
if (await config.shouldUseBlocklists()) { if (await config.shouldUseBlocklists()) {
// Preload adblocker to inject scripts/styles // Preload adblocker to inject scripts/styles
require('@cliqz/adblocker-electron-preload'); injectCliqzPreload();
// eslint-disable-next-line @typescript-eslint/await-thenable // eslint-disable-next-line @typescript-eslint/await-thenable
} else if ((await config.get('blocker')) === config.blockers.InPlayer) { } else if ((await config.get('blocker')) === config.blockers.InPlayer) {
require('./inject.js'); inject();
} }
}; };

View File

@ -1,5 +1,7 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { ConfigType } from '../../config/dynamic';
import type { FastAverageColorResult } from 'fast-average-color'; import type { FastAverageColorResult } from 'fast-average-color';
function hexToHSL(H: string) { function hexToHSL(H: string) {
@ -71,7 +73,7 @@ function changeElementColor(element: HTMLElement | null, hue: number, saturation
} }
} }
export default () => { export default (_: ConfigType<'album-color-theme'>) => {
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) { for (const mutation of mutationsList) {
if (mutation.type === 'attributes') { if (mutation.type === 'attributes') {

View File

@ -7,11 +7,13 @@ import config from './config';
import promptOptions from '../../providers/prompt-options'; import promptOptions from '../../providers/prompt-options';
import configOptions from '../../config/defaults'; import configOptions from '../../config/defaults';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
const defaultOptions = configOptions.plugins.crossfade; const defaultOptions = configOptions.plugins.crossfade;
export default (win: BrowserWindow) => [ export default (win: BrowserWindow): MenuTemplate => [
{ {
label: 'Advanced', label: 'Advanced',
async click() { async click() {

View File

@ -7,6 +7,7 @@ import { clear, connect, isConnected, registerRefresh } from './back';
import { setMenuOptions } from '../../config/plugins'; import { setMenuOptions } from '../../config/plugins';
import promptOptions from '../../providers/prompt-options'; import promptOptions from '../../providers/prompt-options';
import { singleton } from '../../providers/decorators'; import { singleton } from '../../providers/decorators';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
@ -16,14 +17,14 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
type DiscordOptions = ConfigType<'discord'>; type DiscordOptions = ConfigType<'discord'>;
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void) => { export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void): MenuTemplate => {
registerRefreshOnce(refreshMenu); registerRefreshOnce(refreshMenu);
return [ return [
{ {
label: isConnected() ? 'Connected' : 'Reconnect', label: isConnected() ? 'Connected' : 'Reconnect',
enabled: !isConnected(), enabled: !isConnected(),
click: connect, click: () => connect(),
}, },
{ {
label: 'Auto reconnect', label: 'Auto reconnect',

View File

@ -15,7 +15,7 @@ import type { ConfigType } from '../../config/dynamic';
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u; const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false; let revRomanized = false;
export type LyricGeniusType = ConfigType<'lyric-genius'>; export type LyricGeniusType = ConfigType<'lyrics-genius'>;
export default (win: BrowserWindow, options: LyricGeniusType) => { export default (win: BrowserWindow, options: LyricGeniusType) => {
if (options.romanizedLyrics) { if (options.romanizedLyrics) {

View File

@ -3,8 +3,9 @@ import { BrowserWindow, MenuItem } from 'electron';
import { LyricGeniusType, toggleRomanized } from './back'; import { LyricGeniusType, toggleRomanized } from './back';
import { setOptions } from '../../config/plugins'; import { setOptions } from '../../config/plugins';
import { MenuTemplate } from '../../menu';
export default (_: BrowserWindow, options: LyricGeniusType) => [ export default (_: BrowserWindow, options: LyricGeniusType): MenuTemplate => [
{ {
label: 'Romanized Lyrics', label: 'Romanized Lyrics',
type: 'checkbox', type: 'checkbox',

View File

@ -1,27 +0,0 @@
import { Actions, triggerAction } from '../utils';
export const CHANNEL = 'navigation';
export const ACTIONS = Actions;
export function goToNextPage() {
triggerAction(CHANNEL, Actions.NEXT);
}
// for HTML
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
(global as any).goToNextPage = goToNextPage;
export function goToPreviousPage() {
triggerAction(CHANNEL, Actions.BACK);
}
// for HTML
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
(global as any).goToPreviousPage = goToPreviousPage;
export default {
CHANNEL,
ACTIONS,
actions: {
goToNextPage,
goToPreviousPage,
},
};

View File

@ -2,38 +2,12 @@ import path from 'node:path';
import { BrowserWindow } from 'electron'; import { BrowserWindow } from 'electron';
import { ACTIONS, CHANNEL } from './actions'; import { injectCSS } from '../utils';
import { injectCSS, listenAction } from '../utils';
export function handle(win: BrowserWindow) { export function handle(win: BrowserWindow) {
injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => { injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => {
win.webContents.send('navigation-css-ready'); win.webContents.send('navigation-css-ready');
}); });
listenAction(CHANNEL, (_, action) => {
switch (action) {
case ACTIONS.NEXT: {
if (win.webContents.canGoForward()) {
win.webContents.goForward();
}
break;
}
case ACTIONS.BACK: {
if (win.webContents.canGoBack()) {
win.webContents.goBack();
}
break;
}
default: {
console.log('Unknown action: ' + action);
}
}
});
} }
export default handle; export default handle;

View File

@ -1,6 +1,6 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
onclick="goToPreviousPage()" onclick="history.back()"
role="tab" role="tab"
tab-id="FEmusic_back" tab-id="FEmusic_back"
> >

View File

@ -1,6 +1,6 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
onclick="goToNextPage()" onclick="history.forward()"
role="tab" role="tab"
tab-id="FEmusic_next" tab-id="FEmusic_next"
> >

View File

@ -6,11 +6,13 @@ import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
import config from './config'; import config from './config';
import { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [ const getMenu = (options: ConfigType<'notifications'>): MenuTemplate => {
...(is.linux() if (is.linux()) {
? [ return [
{ {
label: 'Notification Priority', label: 'Notification Priority',
submenu: urgencyLevels.map((level) => ({ submenu: urgencyLevels.map((level) => ({
@ -19,11 +21,10 @@ export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
checked: options.urgency === level.value, checked: options.urgency === level.value,
click: () => config.set('urgency', level.value), click: () => config.set('urgency', level.value),
})), })),
}, }
] ];
: []), } else if (is.windows()) {
...(is.windows() return [
? [
{ {
label: 'Interactive Notifications', label: 'Interactive Notifications',
type: 'checkbox', type: 'checkbox',
@ -59,8 +60,14 @@ export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
label: 'Style', label: 'Style',
submenu: getToastStyleMenuItems(options), submenu: getToastStyleMenuItems(options),
}, },
] ];
: []), } else {
return [];
}
};
export default (_win: BrowserWindow, options: ConfigType<'notifications'>): MenuTemplate => [
...getMenu(options),
{ {
label: 'Show notification on unpause', label: 'Show notification on unpause',
type: 'checkbox', type: 'checkbox',
@ -79,8 +86,8 @@ export function getToastStyleMenuItems(options: ConfigType<'notifications'>) {
type: 'radio', type: 'radio',
checked: options.toastStyle === index, checked: options.toastStyle === index,
click: () => config.set('toastStyle', index), click: () => config.set('toastStyle', index),
}; } satisfies Electron.MenuItemConstructorOptions;
} }
return array; return array as Electron.MenuItemConstructorOptions[];
} }

View File

@ -10,8 +10,6 @@ import defaultConfig from '../../config/defaults';
import type { GetPlayerResponse } from '../../types/get-player-response'; import type { GetPlayerResponse } from '../../types/get-player-response';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
let videoID: string;
export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => { export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => {
const { apiURL, categories } = { const { apiURL, categories } = {
...defaultConfig.plugins.sponsorblock, ...defaultConfig.plugins.sponsorblock,
@ -19,14 +17,13 @@ export default (win: BrowserWindow, options: ConfigType<'sponsorblock'>) => {
}; };
ipcMain.on('video-src-changed', async (_, data: GetPlayerResponse) => { ipcMain.on('video-src-changed', async (_, data: GetPlayerResponse) => {
videoID = data?.videoDetails?.videoId; const segments = await fetchSegments(apiURL, categories, data?.videoDetails?.videoId);
const segments = await fetchSegments(apiURL, categories);
win.webContents.send('sponsorblock-skip', segments); win.webContents.send('sponsorblock-skip', segments);
}); });
}; };
const fetchSegments = async (apiURL: string, categories: string[]) => { const fetchSegments = async (apiURL: string, categories: string[], videoId: string) => {
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify( const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoId}&categories=${JSON.stringify(
categories, categories,
)}`; )}`;
try { try {

View File

@ -5,17 +5,46 @@ import { BrowserWindow, nativeImage } from 'electron';
import getSongControls from '../../providers/song-controls'; import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info'; import registerCallback, { SongInfo } from '../../providers/song-info';
let controls: {
playPause: () => void;
next: () => void;
previous: () => void;
};
let currentSongInfo: SongInfo;
export default (win: BrowserWindow) => { export default (win: BrowserWindow) => {
let currentSongInfo: SongInfo;
const { playPause, next, previous } = getSongControls(win); const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };
const setThumbar = (win: BrowserWindow, songInfo: SongInfo) => {
// Wait for song to start before setting thumbar
if (!songInfo?.title) {
return;
}
// Win32 require full rewrite of components
win.setThumbarButtons([
{
tooltip: 'Previous',
icon: nativeImage.createFromPath(get('previous')),
click() {
previous();
},
}, {
tooltip: 'Play/Pause',
// Update icon based on play state
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
click() {
playPause();
},
}, {
tooltip: 'Next',
icon: nativeImage.createFromPath(get('next')),
click() {
next();
},
},
]);
};
// Util
const get = (kind: string) => {
return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
};
registerCallback((songInfo) => { registerCallback((songInfo) => {
// Update currentsonginfo for win.on('show') // Update currentsonginfo for win.on('show')
@ -29,39 +58,3 @@ export default (win: BrowserWindow) => {
setThumbar(win, currentSongInfo); setThumbar(win, currentSongInfo);
}); });
}; };
function setThumbar(win: BrowserWindow, songInfo: SongInfo) {
// Wait for song to start before setting thumbar
if (!songInfo?.title) {
return;
}
// Win32 require full rewrite of components
win.setThumbarButtons([
{
tooltip: 'Previous',
icon: nativeImage.createFromPath(get('previous')),
click() {
controls.previous();
},
}, {
tooltip: 'Play/Pause',
// Update icon based on play state
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')),
click() {
controls.playPause();
},
}, {
tooltip: 'Next',
icon: nativeImage.createFromPath(get('next')),
click() {
controls.next();
},
},
]);
}
// Util
function get(kind: string) {
return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
}

View File

@ -3,31 +3,32 @@ import { TouchBar, NativeImage, BrowserWindow } from 'electron';
import registerCallback from '../../providers/song-info'; import registerCallback from '../../providers/song-info';
import getSongControls from '../../providers/song-controls'; import getSongControls from '../../providers/song-controls';
const { export default (win: BrowserWindow) => {
const {
TouchBarButton, TouchBarButton,
TouchBarLabel, TouchBarLabel,
TouchBarSpacer, TouchBarSpacer,
TouchBarSegmentedControl, TouchBarSegmentedControl,
TouchBarScrubber, TouchBarScrubber,
} = TouchBar; } = TouchBar;
// Songtitle label // Songtitle label
const songTitle = new TouchBarLabel({ const songTitle = new TouchBarLabel({
label: '', label: '',
}); });
// This will store the song controls once available // This will store the song controls once available
let controls: (() => void)[] = []; let controls: (() => void)[] = [];
// This will store the song image once available // This will store the song image once available
const songImage: { const songImage: {
icon?: NativeImage; icon?: NativeImage;
} = {}; } = {};
// Pause/play button // Pause/play button
const pausePlayButton = new TouchBarButton({}); const pausePlayButton = new TouchBarButton({});
// The song control buttons (control functions are in the same order) // The song control buttons (control functions are in the same order)
const buttons = new TouchBarSegmentedControl({ const buttons = new TouchBarSegmentedControl({
mode: 'buttons', mode: 'buttons',
segments: [ segments: [
new TouchBarButton({ new TouchBarButton({
@ -45,10 +46,10 @@ const buttons = new TouchBarSegmentedControl({
}), }),
], ],
change: (i) => controls[i](), change: (i) => controls[i](),
}); });
// This is the touchbar object, this combines everything with proper layout // This is the touchbar object, this combines everything with proper layout
const touchBar = new TouchBar({ const touchBar = new TouchBar({
items: [ items: [
new TouchBarScrubber({ new TouchBarScrubber({
items: [songImage, songTitle], items: [songImage, songTitle],
@ -59,9 +60,9 @@ const touchBar = new TouchBar({
}), }),
buttons, buttons,
], ],
}); });
export default (win: BrowserWindow) => {
const { playPause, next, previous, dislike, like } = getSongControls(win); const { playPause, next, previous, dislike, like } = getSongControls(win);
// If the page is ready, register the callback // If the page is ready, register the callback

View File

@ -3,7 +3,10 @@ import path from 'node:path';
import { ipcMain, ipcRenderer } from 'electron'; import { ipcMain, ipcRenderer } from 'electron';
import is from 'electron-is';
import { ValueOf } from '../utils/type-utils'; import { ValueOf } from '../utils/type-utils';
import defaultConfig from '../config/defaults';
// Creates a DOM element from an HTML string // Creates a DOM element from an HTML string
export const ElementFromHtml = (html: string): HTMLElement => { export const ElementFromHtml = (html: string): HTMLElement => {
@ -64,11 +67,15 @@ const setupCssInjection = (webContents: Electron.WebContents) => {
}); });
}; };
export const getAllPlugins = () => { export const getAvailablePluginNames = () => {
const isDirectory = (source: fs.PathLike) => fs.lstatSync(source).isDirectory(); return Object.keys(defaultConfig.plugins).filter((name) => {
return fs if (is.windows() && name === 'touchbar') {
.readdirSync(__dirname) return false;
.map((name) => path.join(__dirname, name)) } else if (is.macOS() && name === 'taskbar-mediacontrol') {
.filter(isDirectory) return false;
.map((name) => path.basename(name)); } else if (is.linux() && (name === 'taskbar-mediacontrol' || name === 'touchbar')) {
return false;
}
return true;
});
}; };

View File

@ -2,75 +2,104 @@ import { ipcRenderer } from 'electron';
import is from 'electron-is'; import is from 'electron-is';
import config from './config'; import config from './config';
import { fileExists } from './plugins/utils';
import setupSongInfo from './providers/song-info-front'; import setupSongInfo from './providers/song-info-front';
import { setupSongControls } from './providers/song-controls-front'; import { setupSongControls } from './providers/song-controls-front';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import albumColorThemeRenderer from './plugins/album-color-theme/front';
import audioCompressorRenderer from './plugins/audio-compressor/front';
import bypassAgeRestrictionsRenderer from './plugins/bypass-age-restrictions/front';
import captionsSelectorRenderer from './plugins/captions-selector/front';
import compactSidebarRenderer from './plugins/compact-sidebar/front';
import crossfadeRenderer from './plugins/crossfade/front';
import disableAutoplayRenderer from './plugins/disable-autoplay/front';
import downloaderRenderer from './plugins/downloader/front';
import exponentialVolumeRenderer from './plugins/exponential-volume/front';
import inAppMenuRenderer from './plugins/in-app-menu/front';
import lyricsGeniusRenderer from './plugins/lyrics-genius/front';
import navigationRenderer from './plugins/navigation/front';
import noGoogleLogin from './plugins/no-google-login/front';
import pictureInPictureRenderer from './plugins/picture-in-picture/front';
import playbackSpeedRenderer from './plugins/playback-speed/front';
import preciseVolumeRenderer from './plugins/precise-volume/front';
import qualityChangerRenderer from './plugins/quality-changer/front';
import skipSilencesRenderer from './plugins/skip-silences/front';
import sponsorblockRenderer from './plugins/sponsorblock/front';
import videoToggleRenderer from './plugins/video-toggle/front';
import visualizerRenderer from './plugins/visualizer/front';
const plugins = config.plugins.getEnabled(); import adblockerPreload from './plugins/adblocker/preload';
import preciseVolumePreload from './plugins/precise-volume/preload';
import type { ConfigType, OneOfDefaultConfigKey } from './config/dynamic';
type PluginMapper<Type extends 'renderer' | 'preload' | 'backend'> = {
[Key in OneOfDefaultConfigKey]?: (
Type extends 'renderer' ? (options: ConfigType<Key>) => (Promise<void> | void) :
Type extends 'preload' ? () => (Promise<void> | void) :
never
)
};
const rendererPlugins: PluginMapper<'renderer'> = {
'album-color-theme': albumColorThemeRenderer,
'audio-compressor': audioCompressorRenderer,
'bypass-age-restrictions': bypassAgeRestrictionsRenderer,
'captions-selector': captionsSelectorRenderer,
'compact-sidebar': compactSidebarRenderer,
'crossfade': crossfadeRenderer,
'disable-autoplay': disableAutoplayRenderer,
'downloader': downloaderRenderer,
'exponential-volume': exponentialVolumeRenderer,
'in-app-menu': inAppMenuRenderer,
'lyrics-genius': lyricsGeniusRenderer,
'navigation': navigationRenderer,
'no-google-login': noGoogleLogin,
'picture-in-picture': pictureInPictureRenderer,
'playback-speed': playbackSpeedRenderer,
'precise-volume': preciseVolumeRenderer,
'quality-changer': qualityChangerRenderer,
'skip-silences': skipSilencesRenderer,
'sponsorblock': sponsorblockRenderer,
'video-toggle': videoToggleRenderer,
'visualizer': visualizerRenderer,
};
const preloadPlugins: PluginMapper<'preload'> = {
'adblocker': adblockerPreload,
'precise-volume': preciseVolumePreload,
};
const enabledPluginNameAndOptions = config.plugins.getEnabled();
const $ = document.querySelector.bind(document); const $ = document.querySelector.bind(document);
let api: Element | null = null; let api: Element | null = null;
interface Actions { enabledPluginNameAndOptions.forEach(async ([plugin, options]) => {
CHANNEL: string; if (Object.hasOwn(preloadPlugins, plugin)) {
ACTIONS: Record<string, string>, const handler = preloadPlugins[plugin];
actions: Record<string, () => void>, try {
} await handler?.();
} catch (error) {
plugins.forEach(async ([plugin, options]) => { console.error(`Error in plugin "${plugin}": ${String(error)}`);
const preloadPath = await ipcRenderer.invoke( }
'getPath',
__dirname,
'plugins',
plugin,
'preload.js',
) as string;
fileExists(preloadPath, () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
const run = require(preloadPath).default as (config: typeof options) => Promise<void>;
run(options);
});
const actionPath = await ipcRenderer.invoke(
'getPath',
__dirname,
'plugins',
plugin,
'actions.js',
) as string;
fileExists(actionPath, () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const actions = (require(actionPath) as Actions).actions ?? {};
// TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
for (const actionName of Object.keys(actions)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
(global as any)[actionName] = actions[actionName];
} }
});
}); });
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
plugins.forEach(async ([plugin, options]) => { enabledPluginNameAndOptions.forEach(async ([pluginName, options]) => {
const pluginPath = await ipcRenderer.invoke( if (Object.hasOwn(rendererPlugins, pluginName)) {
'getPath', const handler = rendererPlugins[pluginName];
__dirname, try {
'plugins', await handler?.(options as never);
plugin, } catch (error) {
'front.js', console.error(`Error in plugin "${pluginName}": ${String(error)}`);
) as string; }
fileExists(pluginPath, () => { }
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
const run = require(pluginPath).default as (config: typeof options) => Promise<void>;
run(options);
});
}); });
// Wait for complete load of youtube api // Wait for complete load of YouTube api
listenForApiLoad(); listenForApiLoad();
// Inject song-info provider // Inject song-info provider