mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
refactor: remove dynamic require (partial of #2)
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -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',
|
||||||
|
|||||||
@ -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> } = {};
|
||||||
|
|||||||
86
index.ts
86
index.ts
@ -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()) {
|
||||||
console.log('Loaded plugin - ' + plugin);
|
try {
|
||||||
const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
|
if (Object.hasOwn(mainPlugins, plugin)) {
|
||||||
fileExists(pluginPath, () => {
|
console.log('Loaded plugin - ' + plugin);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-member-access
|
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
|
||||||
const handle = require(pluginPath).default as (window: BrowserWindow, option: typeof options) => void;
|
if (handler) {
|
||||||
handle(win, options);
|
handler(win, options as never);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} 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
56
menu.ts
@ -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);
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
3
plugins/adblocker/inject-cliqz-preload.ts
Normal file
3
plugins/adblocker/inject-cliqz-preload.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default () => {
|
||||||
|
require('@cliqz/adblocker-electron-preload');
|
||||||
|
};
|
||||||
3
plugins/adblocker/inject.d.ts
vendored
Normal file
3
plugins/adblocker/inject.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const inject: () => void;
|
||||||
|
|
||||||
|
export default inject;
|
||||||
@ -7,428 +7,429 @@
|
|||||||
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;
|
||||||
//
|
//
|
||||||
if (o.playerResponse) {
|
if (o.playerResponse) {
|
||||||
delete o.playerResponse.playerAds;
|
delete o.playerResponse.playerAds;
|
||||||
delete o.playerResponse.adPlacements;
|
delete o.playerResponse.adPlacements;
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
||||||
JSON.parse = new Proxy(JSON.parse, {
|
|
||||||
apply() {
|
|
||||||
return pruner(Reflect.apply(...arguments));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Response.prototype.json = new Proxy(Response.prototype.json, {
|
|
||||||
apply() {
|
|
||||||
return Reflect.apply(...arguments).then((o) => pruner(o));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
let cValue = 'undefined';
|
|
||||||
const chain = 'playerResponse.adPlacements';
|
|
||||||
const thisScript = document.currentScript;
|
|
||||||
//
|
|
||||||
switch (cValue) {
|
|
||||||
case 'null': {
|
|
||||||
cValue = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "''": {
|
|
||||||
cValue = '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'true': {
|
|
||||||
cValue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'false': {
|
|
||||||
cValue = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'undefined': {
|
|
||||||
cValue = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'noopFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'trueFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'falseFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
if (/^\d+$/.test(cValue)) {
|
|
||||||
cValue = Number.parseFloat(cValue);
|
|
||||||
//
|
|
||||||
if (isNaN(cValue)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(cValue) > 0x7F_FF) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
let aborted = false;
|
|
||||||
const mustAbort = function (v) {
|
|
||||||
if (aborted) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
aborted
|
|
||||||
= v !== undefined
|
|
||||||
&& v !== null
|
|
||||||
&& cValue !== undefined
|
|
||||||
&& cValue !== null
|
|
||||||
&& typeof v !== typeof cValue;
|
|
||||||
return aborted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support multiple trappers for the same property:
|
|
||||||
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
|
||||||
*/
|
|
||||||
|
|
||||||
const trapProp = function (owner, prop, configurable, handler) {
|
|
||||||
if (handler.init(owner[prop]) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
|
||||||
let previousGetter;
|
|
||||||
let previousSetter;
|
|
||||||
if (odesc instanceof Object) {
|
|
||||||
if (odesc.configurable === false) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (odesc.get instanceof Function) {
|
//
|
||||||
previousGetter = odesc.get;
|
return o;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (odesc.set instanceof Function) {
|
JSON.parse = new Proxy(JSON.parse, {
|
||||||
previousSetter = odesc.set;
|
apply() {
|
||||||
}
|
return pruner(Reflect.apply(...arguments));
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
Object.defineProperty(owner, prop, {
|
|
||||||
configurable,
|
|
||||||
get() {
|
|
||||||
if (previousGetter !== undefined) {
|
|
||||||
previousGetter();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
return handler.getter();
|
|
||||||
},
|
|
||||||
set(a) {
|
|
||||||
if (previousSetter !== undefined) {
|
|
||||||
previousSetter(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
handler.setter(a);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const trapChain = function (owner, chain) {
|
Response.prototype.json = new Proxy(Response.prototype.json, {
|
||||||
const pos = chain.indexOf('.');
|
apply() {
|
||||||
if (pos === -1) {
|
return Reflect.apply(...arguments).then((o) => pruner(o));
|
||||||
trapProp(owner, chain, false, {
|
},
|
||||||
v: undefined,
|
});
|
||||||
getter() {
|
}
|
||||||
return document.currentScript === thisScript ? this.v : cValue;
|
|
||||||
},
|
(function () {
|
||||||
setter(a) {
|
let cValue = 'undefined';
|
||||||
if (mustAbort(a) === false) {
|
const chain = 'playerResponse.adPlacements';
|
||||||
|
const thisScript = document.currentScript;
|
||||||
|
//
|
||||||
|
switch (cValue) {
|
||||||
|
case 'null': {
|
||||||
|
cValue = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "''": {
|
||||||
|
cValue = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'true': {
|
||||||
|
cValue = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'false': {
|
||||||
|
cValue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'undefined': {
|
||||||
|
cValue = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'noopFunc': {
|
||||||
|
cValue = function () {
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'trueFunc': {
|
||||||
|
cValue = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'falseFunc': {
|
||||||
|
cValue = function () {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if (/^\d+$/.test(cValue)) {
|
||||||
|
cValue = Number.parseFloat(cValue);
|
||||||
|
//
|
||||||
|
if (isNaN(cValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cValue = a;
|
if (Math.abs(cValue) > 0x7F_FF) {
|
||||||
},
|
return;
|
||||||
init(v) {
|
}
|
||||||
if (mustAbort(v)) {
|
} else {
|
||||||
return false;
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
let aborted = false;
|
||||||
|
const mustAbort = function (v) {
|
||||||
|
if (aborted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
aborted
|
||||||
|
= v !== undefined
|
||||||
|
&& v !== null
|
||||||
|
&& cValue !== undefined
|
||||||
|
&& cValue !== null
|
||||||
|
&& typeof v !== typeof cValue;
|
||||||
|
return aborted;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support multiple trappers for the same property:
|
||||||
|
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
||||||
|
*/
|
||||||
|
|
||||||
|
const trapProp = function (owner, prop, configurable, handler) {
|
||||||
|
if (handler.init(owner[prop]) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
||||||
|
let previousGetter;
|
||||||
|
let previousSetter;
|
||||||
|
if (odesc instanceof Object) {
|
||||||
|
if (odesc.configurable === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (odesc.get instanceof Function) {
|
||||||
|
previousGetter = odesc.get;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (odesc.set instanceof Function) {
|
||||||
|
previousSetter = odesc.set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
Object.defineProperty(owner, prop, {
|
||||||
|
configurable,
|
||||||
|
get() {
|
||||||
|
if (previousGetter !== undefined) {
|
||||||
|
previousGetter();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
return handler.getter();
|
||||||
|
},
|
||||||
|
set(a) {
|
||||||
|
if (previousSetter !== undefined) {
|
||||||
|
previousSetter(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
handler.setter(a);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trapChain = function (owner, chain) {
|
||||||
|
const pos = chain.indexOf('.');
|
||||||
|
if (pos === -1) {
|
||||||
|
trapProp(owner, chain, false, {
|
||||||
|
v: undefined,
|
||||||
|
getter() {
|
||||||
|
return document.currentScript === thisScript ? this.v : cValue;
|
||||||
|
},
|
||||||
|
setter(a) {
|
||||||
|
if (mustAbort(a) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cValue = a;
|
||||||
|
},
|
||||||
|
init(v) {
|
||||||
|
if (mustAbort(v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
this.v = v;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
const prop = chain.slice(0, pos);
|
||||||
|
const v = owner[prop];
|
||||||
|
//
|
||||||
|
chain = chain.slice(pos + 1);
|
||||||
|
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
||||||
|
trapChain(v, chain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
trapProp(owner, prop, true, {
|
||||||
|
v: undefined,
|
||||||
|
getter() {
|
||||||
|
return this.v;
|
||||||
|
},
|
||||||
|
setter(a) {
|
||||||
|
this.v = a;
|
||||||
|
if (a instanceof Object) {
|
||||||
|
trapChain(a, chain);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init(v) {
|
||||||
this.v = v;
|
this.v = v;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//
|
};
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
const prop = chain.slice(0, pos);
|
trapChain(window, chain);
|
||||||
const v = owner[prop];
|
})();
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
let cValue = 'undefined';
|
||||||
|
const thisScript = document.currentScript;
|
||||||
|
const chain = 'ytInitialPlayerResponse.adPlacements';
|
||||||
//
|
//
|
||||||
chain = chain.slice(pos + 1);
|
switch (cValue) {
|
||||||
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
case 'null': {
|
||||||
trapChain(v, chain);
|
cValue = null;
|
||||||
return;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
trapProp(owner, prop, true, {
|
|
||||||
v: undefined,
|
|
||||||
getter() {
|
|
||||||
return this.v;
|
|
||||||
},
|
|
||||||
setter(a) {
|
|
||||||
this.v = a;
|
|
||||||
if (a instanceof Object) {
|
|
||||||
trapChain(a, chain);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init(v) {
|
|
||||||
this.v = v;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
trapChain(window, chain);
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
let cValue = 'undefined';
|
|
||||||
const thisScript = document.currentScript;
|
|
||||||
const chain = 'ytInitialPlayerResponse.adPlacements';
|
|
||||||
//
|
|
||||||
switch (cValue) {
|
|
||||||
case 'null': {
|
|
||||||
cValue = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "''": {
|
|
||||||
cValue = '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'true': {
|
|
||||||
cValue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'false': {
|
|
||||||
cValue = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'undefined': {
|
|
||||||
cValue = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'noopFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'trueFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'falseFunc': {
|
|
||||||
cValue = function () {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
if (/^\d+$/.test(cValue)) {
|
|
||||||
cValue = Number.parseFloat(cValue);
|
|
||||||
//
|
|
||||||
if (isNaN(cValue)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(cValue) > 0x7F_FF) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
let aborted = false;
|
|
||||||
const mustAbort = function (v) {
|
|
||||||
if (aborted) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
aborted
|
|
||||||
= v !== undefined
|
|
||||||
&& v !== null
|
|
||||||
&& cValue !== undefined
|
|
||||||
&& cValue !== null
|
|
||||||
&& typeof v !== typeof cValue;
|
|
||||||
return aborted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Support multiple trappers for the same property:
|
|
||||||
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
|
||||||
*/
|
|
||||||
|
|
||||||
const trapProp = function (owner, prop, configurable, handler) {
|
|
||||||
if (handler.init(owner[prop]) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
|
||||||
let previousGetter;
|
|
||||||
let previousSetter;
|
|
||||||
if (odesc instanceof Object) {
|
|
||||||
if (odesc.configurable === false) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (odesc.get instanceof Function) {
|
case "''": {
|
||||||
previousGetter = odesc.get;
|
cValue = '';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (odesc.set instanceof Function) {
|
case 'true': {
|
||||||
previousSetter = odesc.set;
|
cValue = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//
|
case 'false': {
|
||||||
Object.defineProperty(owner, prop, {
|
cValue = false;
|
||||||
configurable,
|
break;
|
||||||
get() {
|
}
|
||||||
if (previousGetter !== undefined) {
|
|
||||||
previousGetter();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
case 'undefined': {
|
||||||
return handler.getter();
|
cValue = undefined;
|
||||||
},
|
break;
|
||||||
set(a) {
|
}
|
||||||
if (previousSetter !== undefined) {
|
|
||||||
previousSetter(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
case 'noopFunc': {
|
||||||
handler.setter(a);
|
cValue = function () {
|
||||||
},
|
};
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const trapChain = function (owner, chain) {
|
break;
|
||||||
const pos = chain.indexOf('.');
|
}
|
||||||
if (pos === -1) {
|
|
||||||
trapProp(owner, chain, false, {
|
case 'trueFunc': {
|
||||||
v: undefined,
|
cValue = function () {
|
||||||
getter() {
|
return true;
|
||||||
return document.currentScript === thisScript ? this.v : cValue;
|
};
|
||||||
},
|
|
||||||
setter(a) {
|
break;
|
||||||
if (mustAbort(a) === false) {
|
}
|
||||||
|
|
||||||
|
case 'falseFunc': {
|
||||||
|
cValue = function () {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if (/^\d+$/.test(cValue)) {
|
||||||
|
cValue = Number.parseFloat(cValue);
|
||||||
|
//
|
||||||
|
if (isNaN(cValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cValue = a;
|
if (Math.abs(cValue) > 0x7F_FF) {
|
||||||
},
|
return;
|
||||||
init(v) {
|
}
|
||||||
if (mustAbort(v)) {
|
} else {
|
||||||
return false;
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
let aborted = false;
|
||||||
|
const mustAbort = function (v) {
|
||||||
|
if (aborted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
aborted
|
||||||
|
= v !== undefined
|
||||||
|
&& v !== null
|
||||||
|
&& cValue !== undefined
|
||||||
|
&& cValue !== null
|
||||||
|
&& typeof v !== typeof cValue;
|
||||||
|
return aborted;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Support multiple trappers for the same property:
|
||||||
|
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
||||||
|
*/
|
||||||
|
|
||||||
|
const trapProp = function (owner, prop, configurable, handler) {
|
||||||
|
if (handler.init(owner[prop]) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
||||||
|
let previousGetter;
|
||||||
|
let previousSetter;
|
||||||
|
if (odesc instanceof Object) {
|
||||||
|
if (odesc.configurable === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (odesc.get instanceof Function) {
|
||||||
|
previousGetter = odesc.get;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (odesc.set instanceof Function) {
|
||||||
|
previousSetter = odesc.set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
Object.defineProperty(owner, prop, {
|
||||||
|
configurable,
|
||||||
|
get() {
|
||||||
|
if (previousGetter !== undefined) {
|
||||||
|
previousGetter();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
return handler.getter();
|
||||||
|
},
|
||||||
|
set(a) {
|
||||||
|
if (previousSetter !== undefined) {
|
||||||
|
previousSetter(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
handler.setter(a);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trapChain = function (owner, chain) {
|
||||||
|
const pos = chain.indexOf('.');
|
||||||
|
if (pos === -1) {
|
||||||
|
trapProp(owner, chain, false, {
|
||||||
|
v: undefined,
|
||||||
|
getter() {
|
||||||
|
return document.currentScript === thisScript ? this.v : cValue;
|
||||||
|
},
|
||||||
|
setter(a) {
|
||||||
|
if (mustAbort(a) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cValue = a;
|
||||||
|
},
|
||||||
|
init(v) {
|
||||||
|
if (mustAbort(v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
this.v = v;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
const prop = chain.slice(0, pos);
|
||||||
|
const v = owner[prop];
|
||||||
|
//
|
||||||
|
chain = chain.slice(pos + 1);
|
||||||
|
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
||||||
|
trapChain(v, chain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
trapProp(owner, prop, true, {
|
||||||
|
v: undefined,
|
||||||
|
getter() {
|
||||||
|
return this.v;
|
||||||
|
},
|
||||||
|
setter(a) {
|
||||||
|
this.v = a;
|
||||||
|
if (a instanceof Object) {
|
||||||
|
trapChain(a, chain);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init(v) {
|
||||||
this.v = v;
|
this.v = v;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//
|
};
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
const prop = chain.slice(0, pos);
|
trapChain(window, chain);
|
||||||
const v = owner[prop];
|
})();
|
||||||
//
|
};
|
||||||
chain = chain.slice(pos + 1);
|
|
||||||
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
|
||||||
trapChain(v, chain);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
trapProp(owner, prop, true, {
|
|
||||||
v: undefined,
|
|
||||||
getter() {
|
|
||||||
return this.v;
|
|
||||||
},
|
|
||||||
setter(a) {
|
|
||||||
this.v = a;
|
|
||||||
if (a instanceof Object) {
|
|
||||||
trapChain(a, chain);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init(v) {
|
|
||||||
this.v = v;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
trapChain(window, chain);
|
|
||||||
})();
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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`);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,65 +3,66 @@ 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 {
|
|
||||||
TouchBarButton,
|
|
||||||
TouchBarLabel,
|
|
||||||
TouchBarSpacer,
|
|
||||||
TouchBarSegmentedControl,
|
|
||||||
TouchBarScrubber,
|
|
||||||
} = TouchBar;
|
|
||||||
|
|
||||||
// Songtitle label
|
|
||||||
const songTitle = new TouchBarLabel({
|
|
||||||
label: '',
|
|
||||||
});
|
|
||||||
// This will store the song controls once available
|
|
||||||
let controls: (() => void)[] = [];
|
|
||||||
|
|
||||||
// This will store the song image once available
|
|
||||||
const songImage: {
|
|
||||||
icon?: NativeImage;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
// Pause/play button
|
|
||||||
const pausePlayButton = new TouchBarButton({});
|
|
||||||
|
|
||||||
// The song control buttons (control functions are in the same order)
|
|
||||||
const buttons = new TouchBarSegmentedControl({
|
|
||||||
mode: 'buttons',
|
|
||||||
segments: [
|
|
||||||
new TouchBarButton({
|
|
||||||
label: '⏮',
|
|
||||||
}),
|
|
||||||
pausePlayButton,
|
|
||||||
new TouchBarButton({
|
|
||||||
label: '⏭',
|
|
||||||
}),
|
|
||||||
new TouchBarButton({
|
|
||||||
label: '👎',
|
|
||||||
}),
|
|
||||||
new TouchBarButton({
|
|
||||||
label: '👍',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
change: (i) => controls[i](),
|
|
||||||
});
|
|
||||||
|
|
||||||
// This is the touchbar object, this combines everything with proper layout
|
|
||||||
const touchBar = new TouchBar({
|
|
||||||
items: [
|
|
||||||
new TouchBarScrubber({
|
|
||||||
items: [songImage, songTitle],
|
|
||||||
continuous: false,
|
|
||||||
}),
|
|
||||||
new TouchBarSpacer({
|
|
||||||
size: 'flexible',
|
|
||||||
}),
|
|
||||||
buttons,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (win: BrowserWindow) => {
|
export default (win: BrowserWindow) => {
|
||||||
|
const {
|
||||||
|
TouchBarButton,
|
||||||
|
TouchBarLabel,
|
||||||
|
TouchBarSpacer,
|
||||||
|
TouchBarSegmentedControl,
|
||||||
|
TouchBarScrubber,
|
||||||
|
} = TouchBar;
|
||||||
|
|
||||||
|
// Songtitle label
|
||||||
|
const songTitle = new TouchBarLabel({
|
||||||
|
label: '',
|
||||||
|
});
|
||||||
|
// This will store the song controls once available
|
||||||
|
let controls: (() => void)[] = [];
|
||||||
|
|
||||||
|
// This will store the song image once available
|
||||||
|
const songImage: {
|
||||||
|
icon?: NativeImage;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Pause/play button
|
||||||
|
const pausePlayButton = new TouchBarButton({});
|
||||||
|
|
||||||
|
// The song control buttons (control functions are in the same order)
|
||||||
|
const buttons = new TouchBarSegmentedControl({
|
||||||
|
mode: 'buttons',
|
||||||
|
segments: [
|
||||||
|
new TouchBarButton({
|
||||||
|
label: '⏮',
|
||||||
|
}),
|
||||||
|
pausePlayButton,
|
||||||
|
new TouchBarButton({
|
||||||
|
label: '⏭',
|
||||||
|
}),
|
||||||
|
new TouchBarButton({
|
||||||
|
label: '👎',
|
||||||
|
}),
|
||||||
|
new TouchBarButton({
|
||||||
|
label: '👍',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
change: (i) => controls[i](),
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the touchbar object, this combines everything with proper layout
|
||||||
|
const touchBar = new TouchBar({
|
||||||
|
items: [
|
||||||
|
new TouchBarScrubber({
|
||||||
|
items: [songImage, songTitle],
|
||||||
|
continuous: false,
|
||||||
|
}),
|
||||||
|
new TouchBarSpacer({
|
||||||
|
size: 'flexible',
|
||||||
|
}),
|
||||||
|
buttons,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -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;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
135
preload.ts
135
preload.ts
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user