feat: run prettier

This commit is contained in:
JellyBrick
2023-11-30 11:59:27 +09:00
parent 44c42310f1
commit a3104fda4b
116 changed files with 2928 additions and 1254 deletions

View File

@ -32,8 +32,8 @@ export interface DefaultConfig {
startingPage: string; startingPage: string;
overrideUserAgent: boolean; overrideUserAgent: boolean;
themes: string[]; themes: string[];
}, };
plugins: Record<string, unknown>, plugins: Record<string, unknown>;
} }
const defaultConfig: DefaultConfig = { const defaultConfig: DefaultConfig = {

View File

@ -12,7 +12,10 @@ export function getPlugins() {
} }
export function isEnabled(plugin: string) { export function isEnabled(plugin: string) {
const pluginConfig = deepmerge(allPlugins[plugin].config ?? { enabled: false }, (store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {}); const pluginConfig = deepmerge(
allPlugins[plugin].config ?? { enabled: false },
(store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {},
);
return pluginConfig !== undefined && pluginConfig.enabled; return pluginConfig !== undefined && pluginConfig.enabled;
} }
@ -22,7 +25,11 @@ export function isEnabled(plugin: string) {
* @param options Options to set * @param options Options to set
* @param exclude Options to exclude from the options object * @param exclude Options to exclude from the options object
*/ */
export function setOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) { export function setOptions<T>(
plugin: string,
options: T,
exclude: string[] = ['enabled'],
) {
const plugins = store.get('plugins') as Record<string, T>; const plugins = store.get('plugins') as Record<string, T>;
// HACK: This is a workaround for preventing changed options from being overwritten // HACK: This is a workaround for preventing changed options from being overwritten
exclude.forEach((key) => { exclude.forEach((key) => {
@ -39,7 +46,11 @@ export function setOptions<T>(plugin: string, options: T, exclude: string[] = ['
}); });
} }
export function setMenuOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) { export function setMenuOptions<T>(
plugin: string,
options: T,
exclude: string[] = ['enabled'],
) {
setOptions(plugin, options, exclude); setOptions(plugin, options, exclude);
if (store.get('options.restartOnConfigChanges')) { if (store.get('options.restartOnConfigChanges')) {
restart(); restart();

View File

@ -7,10 +7,17 @@ import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
const migrations = { const migrations = {
'>=3.0.0'(store: Conf<Record<string, unknown>>) { '>=3.0.0'(store: Conf<Record<string, unknown>>) {
const discordConfig = store.get('plugins.discord') as Record<string, unknown>; const discordConfig = store.get('plugins.discord') as Record<
string,
unknown
>;
if (discordConfig) { if (discordConfig) {
const oldActivityTimoutEnabled = store.get('plugins.discord.activityTimoutEnabled') as boolean | undefined; const oldActivityTimoutEnabled = store.get(
const oldActivityTimoutTime = store.get('plugins.discord.activityTimoutTime') as number | undefined; 'plugins.discord.activityTimoutEnabled',
) as boolean | undefined;
const oldActivityTimoutTime = store.get(
'plugins.discord.activityTimoutTime',
) as number | undefined;
if (oldActivityTimoutEnabled !== undefined) { if (oldActivityTimoutEnabled !== undefined) {
discordConfig.activityTimeoutEnabled = oldActivityTimoutEnabled; discordConfig.activityTimeoutEnabled = oldActivityTimoutEnabled;
store.set('plugins.discord', discordConfig); store.set('plugins.discord', discordConfig);
@ -93,18 +100,23 @@ const migrations = {
} }
}, },
'>=1.12.0'(store: Conf<Record<string, unknown>>) { '>=1.12.0'(store: Conf<Record<string, unknown>>) {
const options = store.get('plugins.shortcuts') as Record< const options = store.get('plugins.shortcuts') as
string, | Record<
| { string,
action: string; | {
shortcut: unknown; action: string;
}[] shortcut: unknown;
| Record<string, unknown> }[]
> | undefined; | Record<string, unknown>
>
| undefined;
if (options) { if (options) {
let updated = false; let updated = false;
for (const optionType of ['global', 'local']) { for (const optionType of ['global', 'local']) {
if (Object.hasOwn(options, optionType) && Array.isArray(options[optionType])) { if (
Object.hasOwn(options, optionType) &&
Array.isArray(options[optionType])
) {
const optionsArray = options[optionType] as { const optionsArray = options[optionType] as {
action: string; action: string;
shortcut: unknown; shortcut: unknown;

View File

@ -53,33 +53,45 @@ declare module 'custom-electron-prompt' {
export interface CounterPromptOptions extends BasePromptOptions<'counter'> { export interface CounterPromptOptions extends BasePromptOptions<'counter'> {
counterOptions: CounterOptions; counterOptions: CounterOptions;
} }
export interface MultiInputPromptOptions extends BasePromptOptions<'multiInput'> { export interface MultiInputPromptOptions
extends BasePromptOptions<'multiInput'> {
multiInputOptions: InputOptions[]; multiInputOptions: InputOptions[];
} }
export interface KeybindPromptOptions extends BasePromptOptions<'keybind'> { export interface KeybindPromptOptions extends BasePromptOptions<'keybind'> {
keybindOptions: KeybindOptions[]; keybindOptions: KeybindOptions[];
} }
export type PromptOptions<T extends string> = ( export type PromptOptions<T extends string> = T extends 'input'
T extends 'input' ? InputPromptOptions : ? InputPromptOptions
T extends 'select' ? SelectPromptOptions : : T extends 'select'
T extends 'counter' ? CounterPromptOptions : ? SelectPromptOptions
T extends 'keybind' ? KeybindPromptOptions : : T extends 'counter'
T extends 'multiInput' ? MultiInputPromptOptions : ? CounterPromptOptions
never : T extends 'keybind'
); ? KeybindPromptOptions
: T extends 'multiInput'
? MultiInputPromptOptions
: never;
type PromptResult<T extends string> = T extends 'input' ? string : type PromptResult<T extends string> = T extends 'input'
T extends 'select' ? string : ? string
T extends 'counter' ? number : : T extends 'select'
T extends 'keybind' ? { ? string
value: string; : T extends 'counter'
accelerator: string ? number
}[] : : T extends 'keybind'
T extends 'multiInput' ? string[] : ? {
never; value: string;
accelerator: string;
}[]
: T extends 'multiInput'
? string[]
: never;
const prompt: <T extends Type>(options?: PromptOptions<T> & { type: T }, parent?: BrowserWindow) => Promise<PromptResult<T> | null>; const prompt: <T extends Type>(
options?: PromptOptions<T> & { type: T },
parent?: BrowserWindow,
) => Promise<PromptResult<T> | null>;
export default prompt; export default prompt;
} }

View File

@ -1,50 +1,50 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8" />
<title>Cannot load YouTube Music</title> <title>Cannot load YouTube Music</title>
<style> <style>
body { body {
background: #000; background: #000;
} }
.container { .container {
margin: 0; margin: 0;
font-family: Roboto, Arial, sans-serif; font-family: Roboto, Arial, sans-serif;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-right: -50%; margin-right: -50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
text-align: center; text-align: center;
} }
.button { .button {
background: #065fd4; background: #065fd4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: white; color: white;
font: inherit; font: inherit;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
border-radius: 2px; border-radius: 2px;
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
padding: 8px 22px; padding: 8px 22px;
display: inline-block; display: inline-block;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<p>Cannot load YouTube Music… Internet disconnected?</p> <p>Cannot load YouTube Music… Internet disconnected?</p>
<a class="button" href="#" onclick="reload()">Retry</a> <a class="button" href="#" onclick="reload()">Retry</a>
</div> </div>
</body> </body>
</html> </html>

View File

@ -165,7 +165,10 @@ const initHook = (win: BrowserWindow) => {
const mainPlugin = getAllLoadedMainPlugins()[id]; const mainPlugin = getAllLoadedMainPlugins()[id];
if (mainPlugin) { if (mainPlugin) {
if (config.enabled && typeof mainPlugin.backend !== 'function') { if (config.enabled && typeof mainPlugin.backend !== 'function') {
mainPlugin.backend?.onConfigChange?.call(mainPlugin.backend, config); mainPlugin.backend?.onConfigChange?.call(
mainPlugin.backend,
config,
);
} }
} }
@ -282,7 +285,6 @@ async function createMainWindow() {
await loadAllMainPlugins(win); await loadAllMainPlugins(win);
if (windowPosition) { if (windowPosition) {
const { x: windowX, y: windowY } = windowPosition; const { x: windowX, y: windowY } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
@ -317,7 +319,6 @@ async function createMainWindow() {
} }
} }
if (windowMaximized) { if (windowMaximized) {
win.maximize(); win.maximize();
} }

View File

@ -9,9 +9,15 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import type { PluginConfig, PluginDef } from '@/types/plugins'; import type { PluginConfig, PluginDef } from '@/types/plugins';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
const loadedPluginMap: Record<string, PluginDef<unknown, unknown, unknown>> = {}; const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
const createContext = (id: string, win: BrowserWindow): BackendContext<PluginConfig> => ({ const createContext = (
id: string,
win: BrowserWindow,
): BackendContext<PluginConfig> => ({
getConfig: () => getConfig: () =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, allPlugins[id].config ?? { enabled: false },
@ -36,7 +42,7 @@ const createContext = (id: string, win: BrowserWindow): BackendContext<PluginCon
}, },
removeHandler: (event: string) => { removeHandler: (event: string) => {
ipcMain.removeHandler(event); ipcMain.removeHandler(event);
} },
}, },
window: win, window: win,
@ -56,19 +62,15 @@ export const forceUnloadMainPlugin = async (
}); });
if ( if (
hasStopped || hasStopped ||
( (hasStopped === null &&
hasStopped === null && typeof plugin.backend !== 'function' &&
typeof plugin.backend !== 'function' && plugin.backend plugin.backend)
)
) { ) {
delete loadedPluginMap[id]; delete loadedPluginMap[id];
console.log(LoggerPrefix, `"${id}" plugin is unloaded`); console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
return; return;
} else { } else {
console.log( console.log(LoggerPrefix, `Cannot unload "${id}" plugin`);
LoggerPrefix,
`Cannot unload "${id}" plugin`,
);
return Promise.reject(); return Promise.reject();
} }
} catch (err) { } catch (err) {
@ -92,10 +94,9 @@ export const forceLoadMainPlugin = async (
}); });
if ( if (
hasStarted || hasStarted ||
( (hasStarted === null &&
hasStarted === null && typeof plugin.backend !== 'function' &&
typeof plugin.backend !== 'function' && plugin.backend plugin.backend)
)
) { ) {
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
} else { } else {
@ -103,10 +104,7 @@ export const forceLoadMainPlugin = async (
return Promise.reject(); return Promise.reject();
} }
} catch (err) { } catch (err) {
console.error( console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
LoggerPrefix,
`Cannot initialize "${id}" plugin: `,
);
console.trace(err); console.trace(err);
return Promise.reject(err); return Promise.reject(err);
} }
@ -135,7 +133,9 @@ export const unloadAllMainPlugins = async (win: BrowserWindow) => {
} }
}; };
export const getLoadedMainPlugin = (id: string): PluginDef<unknown, unknown, unknown> | undefined => { export const getLoadedMainPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id]; return loadedPluginMap[id];
}; };

View File

@ -11,7 +11,10 @@ import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
const menuTemplateMap: Record<string, MenuItemConstructorOptions[]> = {}; const menuTemplateMap: Record<string, MenuItemConstructorOptions[]> = {};
const createContext = (id: string, win: BrowserWindow): MenuContext<PluginConfig> => ({ const createContext = (
id: string,
win: BrowserWindow,
): MenuContext<PluginConfig> => ({
getConfig: () => getConfig: () =>
deepmerge( deepmerge(
allPlugins[id].config ?? { enabled: false }, allPlugins[id].config ?? { enabled: false },
@ -43,8 +46,7 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
} else { } else {
return; return;
} }
} } else return;
else return;
console.log(LoggerPrefix, `Successfully loaded '${id}::menu'`); console.log(LoggerPrefix, `Successfully loaded '${id}::menu'`);
} catch (err) { } catch (err) {
@ -57,7 +59,10 @@ export const loadAllMenuPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins(); const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(allPlugins)) { for (const [pluginId, pluginDef] of Object.entries(allPlugins)) {
const config = deepmerge(pluginDef.config ?? { enabled: false }, pluginConfigs[pluginId] ?? {}); const config = deepmerge(
pluginDef.config ?? { enabled: false },
pluginConfigs[pluginId] ?? {},
);
if (config.enabled) { if (config.enabled) {
await forceLoadMenuPlugin(pluginId, win); await forceLoadMenuPlugin(pluginId, win);

View File

@ -8,7 +8,10 @@ import config from '@/config';
import type { PreloadContext } from '@/types/contexts'; import type { PreloadContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins'; import type { PluginConfig, PluginDef } from '@/types/plugins';
const loadedPluginMap: Record<string, PluginDef<unknown, unknown, unknown>> = {}; const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
const createContext = (id: string): PreloadContext<PluginConfig> => ({ const createContext = (id: string): PreloadContext<PluginConfig> => ({
getConfig: () => getConfig: () =>
deepmerge( deepmerge(
@ -27,13 +30,7 @@ export const forceUnloadPreloadPlugin = async (id: string) => {
ctx: 'preload', ctx: 'preload',
context: createContext(id), context: createContext(id),
}); });
if ( if (hasStopped || (hasStopped === null && loadedPluginMap[id].preload)) {
hasStopped ||
(
hasStopped === null &&
loadedPluginMap[id].preload
)
) {
console.log(LoggerPrefix, `"${id}" plugin is unloaded`); console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
delete loadedPluginMap[id]; delete loadedPluginMap[id];
} else { } else {
@ -53,20 +50,16 @@ export const forceLoadPreloadPlugin = async (id: string) => {
if ( if (
hasStarted || hasStarted ||
( (hasStarted === null &&
hasStarted === null && typeof plugin.preload !== 'function' &&
typeof plugin.preload !== 'function' && plugin.preload plugin.preload)
)
) { ) {
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
} }
console.log(LoggerPrefix, `"${id}" plugin is loaded`); console.log(LoggerPrefix, `"${id}" plugin is loaded`);
} catch (err) { } catch (err) {
console.error( console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `);
LoggerPrefix,
`Cannot initialize "${id}" plugin: `,
);
console.trace(err); console.trace(err);
} }
}; };
@ -75,7 +68,10 @@ export const loadAllPreloadPlugins = () => {
const pluginConfigs = config.plugins.getPlugins(); const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) { for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) {
const config = deepmerge(pluginDef.config ?? { enable: false }, pluginConfigs[pluginId] ?? {}); const config = deepmerge(
pluginDef.config ?? { enable: false },
pluginConfigs[pluginId] ?? {},
);
if (config.enabled) { if (config.enabled) {
forceLoadPreloadPlugin(pluginId); forceLoadPreloadPlugin(pluginId);
@ -93,7 +89,9 @@ export const unloadAllPreloadPlugins = async () => {
} }
}; };
export const getLoadedPreloadPlugin = (id: string): PluginDef<unknown, unknown, unknown> | undefined => { export const getLoadedPreloadPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id]; return loadedPluginMap[id];
}; };

View File

@ -8,9 +8,14 @@ import type { RendererContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins'; import type { PluginConfig, PluginDef } from '@/types/plugins';
const unregisterStyleMap: Record<string, (() => void)[]> = {}; const unregisterStyleMap: Record<string, (() => void)[]> = {};
const loadedPluginMap: Record<string, PluginDef<unknown, unknown, unknown>> = {}; const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
export const createContext = <Config extends PluginConfig>(id: string): RendererContext<Config> => ({ export const createContext = <Config extends PluginConfig>(
id: string,
): RendererContext<Config> => ({
getConfig: async () => window.ipcRenderer.invoke('get-config', id), getConfig: async () => window.ipcRenderer.invoke('get-config', id),
setConfig: async (newConfig) => { setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('set-config', id, newConfig); await window.ipcRenderer.invoke('set-config', id, newConfig);
@ -19,7 +24,8 @@ export const createContext = <Config extends PluginConfig>(id: string): Renderer
send: (event: string, ...args: unknown[]) => { send: (event: string, ...args: unknown[]) => {
window.ipcRenderer.send(event, ...args); window.ipcRenderer.send(event, ...args);
}, },
invoke: (event: string, ...args: unknown[]) => window.ipcRenderer.invoke(event, ...args), invoke: (event: string, ...args: unknown[]) =>
window.ipcRenderer.invoke(event, ...args),
on: (event: string, listener: CallableFunction) => { on: (event: string, listener: CallableFunction) => {
window.ipcRenderer.on(event, (_, ...args: unknown[]) => { window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
listener(...args); listener(...args);
@ -27,7 +33,7 @@ export const createContext = <Config extends PluginConfig>(id: string): Renderer
}, },
removeAllListeners: (event: string) => { removeAllListeners: (event: string) => {
window.ipcRenderer.removeAllListeners(event); window.ipcRenderer.removeAllListeners(event);
} },
}, },
}); });
@ -40,17 +46,14 @@ export const forceUnloadRendererPlugin = async (id: string) => {
const plugin = rendererPlugins[id]; const plugin = rendererPlugins[id];
if (!plugin) return; if (!plugin) return;
const hasStopped = await stopPlugin(id, plugin, { ctx: 'renderer', context: createContext(id) }); const hasStopped = await stopPlugin(id, plugin, {
ctx: 'renderer',
context: createContext(id),
});
if (plugin?.stylesheets) { if (plugin?.stylesheets) {
document.querySelector(`style#plugin-${id}`)?.remove(); document.querySelector(`style#plugin-${id}`)?.remove();
} }
if ( if (hasStopped || (hasStopped === null && plugin?.renderer)) {
hasStopped ||
(
hasStopped === null &&
plugin?.renderer
)
) {
console.log(LoggerPrefix, `"${id}" plugin is unloaded`); console.log(LoggerPrefix, `"${id}" plugin is unloaded`);
} else { } else {
console.error(LoggerPrefix, `Cannot stop "${id}" plugin`); console.error(LoggerPrefix, `Cannot stop "${id}" plugin`);
@ -69,10 +72,9 @@ export const forceLoadRendererPlugin = async (id: string) => {
if ( if (
hasEvaled || hasEvaled ||
plugin?.stylesheets || plugin?.stylesheets ||
( (hasEvaled === null &&
hasEvaled === null && typeof plugin?.renderer !== 'function' &&
typeof plugin?.renderer !== 'function' && plugin?.renderer plugin?.renderer)
)
) { ) {
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
@ -84,7 +86,10 @@ export const forceLoadRendererPlugin = async (id: string) => {
return styleSheet; return styleSheet;
}); });
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...styleSheetList]; document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
...styleSheetList,
];
} }
console.log(LoggerPrefix, `"${id}" plugin is loaded`); console.log(LoggerPrefix, `"${id}" plugin is loaded`);
@ -97,7 +102,7 @@ export const loadAllRendererPlugins = async () => {
const pluginConfigs = window.mainConfig.plugins.getPlugins(); const pluginConfigs = window.mainConfig.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) { for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) {
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {}) ; const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
if (config.enabled) { if (config.enabled) {
await forceLoadRendererPlugin(pluginId); await forceLoadRendererPlugin(pluginId);
@ -115,7 +120,9 @@ export const unloadAllRendererPlugins = async () => {
} }
}; };
export const getLoadedRendererPlugin = (id: string): PluginDef<unknown, unknown, unknown> | undefined => { export const getLoadedRendererPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id]; return loadedPluginMap[id];
}; };

View File

@ -53,7 +53,9 @@ export const refreshMenu = async (win: BrowserWindow) => {
} }
}; };
export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate> => { export const mainMenuTemplate = async (
win: BrowserWindow,
): Promise<MenuTemplate> => {
const innerRefreshMenu = () => refreshMenu(win); const innerRefreshMenu = () => refreshMenu(win);
await loadAllMenuPlugins(win); await loadAllMenuPlugins(win);
@ -453,7 +455,7 @@ export const mainMenuTemplate = async (win: BrowserWindow): Promise<MenuTemplate
]; ];
}; };
export const setApplicationMenu = async (win: Electron.BrowserWindow) => { export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
const menuTemplate: MenuTemplate = [...await mainMenuTemplate(win)]; const menuTemplate: MenuTemplate = [...(await mainMenuTemplate(win))];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const { name } = app; const { name } = app;
menuTemplate.unshift({ menuTemplate.unshift({

7
src/navigation.d.ts vendored
View File

@ -62,7 +62,10 @@ interface Navigation extends EventTarget {
onnavigateerror: ((this: Navigation, ev: Event) => any) | null; onnavigateerror: ((this: Navigation, ev: Event) => any) | null;
oncurrententrychange: ((this: Navigation, ev: Event) => any) | null; oncurrententrychange: ((this: Navigation, ev: Event) => any) | null;
addEventListener<K extends keyof NavigationEventsMap>(name: K, listener: (event: NavigationEventsMap[K]) => void); addEventListener<K extends keyof NavigationEventsMap>(
name: K,
listener: (event: NavigationEventsMap[K]) => void,
);
} }
declare class NavigateEvent extends Event { declare class NavigateEvent extends Event {
@ -84,5 +87,5 @@ type NavigationHistoryBehavior = 'auto' | 'push' | 'replace';
declare const Navigation: { declare const Navigation: {
prototype: Navigation; prototype: Navigation;
new(): Navigation; new (): Navigation;
}; };

View File

@ -30,19 +30,19 @@ export const loadAdBlockerEngine = async (
if (!fs.existsSync(cacheDirectory)) { if (!fs.existsSync(cacheDirectory)) {
fs.mkdirSync(cacheDirectory); fs.mkdirSync(cacheDirectory);
} }
const cachingOptions const cachingOptions =
= cache && additionalBlockLists.length === 0 cache && additionalBlockLists.length === 0
? { ? {
path: path.join(cacheDirectory, 'adblocker-engine.bin'), path: path.join(cacheDirectory, 'adblocker-engine.bin'),
read: promises.readFile, read: promises.readFile,
write: promises.writeFile, write: promises.writeFile,
} }
: undefined; : undefined;
const lists = [ const lists = [
...( ...((disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
(disableDefaultLists && !Array.isArray(disableDefaultLists)) || (Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0)
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) ? [] : SOURCES ? []
), : SOURCES),
...additionalBlockLists, ...additionalBlockLists,
]; ];
@ -72,4 +72,5 @@ export const unloadAdBlockerEngine = (session: Electron.Session) => {
} }
}; };
export const isBlockerEnabled = (session: Electron.Session) => blocker !== undefined && blocker.isBlockingEnabled(session); export const isBlockerEnabled = (session: Electron.Session) =>
blocker !== undefined && blocker.isBlockingEnabled(session);

View File

@ -1,6 +1,10 @@
import { blockers } from './types'; import { blockers } from './types';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from './blocker'; import {
isBlockerEnabled,
loadAdBlockerEngine,
unloadAdBlockerEngine,
} from './blocker';
import injectCliqzPreload from './injectors/inject-cliqz-preload'; import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject'; import { inject, isInjected } from './injectors/inject';
@ -22,7 +26,7 @@ interface AdblockerConfig {
* Which adblocker to use. * Which adblocker to use.
* @default blockers.InPlayer * @default blockers.InPlayer
*/ */
blocker: typeof blockers[keyof typeof blockers]; blocker: (typeof blockers)[keyof typeof blockers];
/** /**
* Additional list of filters to use. * Additional list of filters to use.
* @example ["https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"] * @example ["https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"]
@ -86,7 +90,10 @@ export default createPlugin({
}, },
async onConfigChange(newConfig) { async onConfigChange(newConfig) {
if (this.mainWindow) { if (this.mainWindow) {
if (newConfig.blocker === blockers.WithBlocklists && !isBlockerEnabled(this.mainWindow.webContents.session)) { if (
newConfig.blocker === blockers.WithBlocklists &&
!isBlockerEnabled(this.mainWindow.webContents.session)
) {
await loadAdBlockerEngine( await loadAdBlockerEngine(
this.mainWindow.webContents.session, this.mainWindow.webContents.session,
newConfig.cache, newConfig.cache,
@ -117,5 +124,5 @@ export default createPlugin({
} }
} }
}, },
} },
}); });

View File

@ -73,8 +73,7 @@ export const inject = () => {
} }
case 'noopFunc': { case 'noopFunc': {
cValue = function () { cValue = function () {};
};
break; break;
} }
@ -103,7 +102,7 @@ export const inject = () => {
return; return;
} }
if (Math.abs(cValue) > 0x7F_FF) { if (Math.abs(cValue) > 0x7f_ff) {
return; return;
} }
} else { } else {
@ -119,12 +118,12 @@ export const inject = () => {
return true; return true;
} }
aborted aborted =
= v !== undefined v !== undefined &&
&& v !== null v !== null &&
&& cValue !== undefined cValue !== undefined &&
&& cValue !== null cValue !== null &&
&& typeof v !== typeof cValue; typeof v !== typeof cValue;
return aborted; return aborted;
}; };
@ -272,8 +271,7 @@ export const inject = () => {
} }
case 'noopFunc': { case 'noopFunc': {
cValue = function () { cValue = function () {};
};
break; break;
} }
@ -302,7 +300,7 @@ export const inject = () => {
return; return;
} }
if (Math.abs(cValue) > 0x7F_FF) { if (Math.abs(cValue) > 0x7f_ff) {
return; return;
} }
} else { } else {
@ -318,12 +316,12 @@ export const inject = () => {
return true; return true;
} }
aborted aborted =
= v !== undefined v !== undefined &&
&& v !== null v !== null &&
&& cValue !== undefined cValue !== undefined &&
&& cValue !== null cValue !== null &&
&& typeof v !== typeof cValue; typeof v !== typeof cValue;
return aborted; return aborted;
}; };

View File

@ -8,7 +8,8 @@ import type { VideoDataChanged } from '@/types/video-data-changed';
export default createPlugin({ export default createPlugin({
name: 'Album Color Theme', name: 'Album Color Theme',
description: 'Applies a dynamic theme and visual effects based on the album color palette', description:
'Applies a dynamic theme and visual effects based on the album color palette',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -62,13 +63,18 @@ export default createPlugin({
l = +(l * 100).toFixed(1); l = +(l * 100).toFixed(1);
//return "hsl(" + h + "," + s + "%," + l + "%)"; //return "hsl(" + h + "," + s + "%," + l + "%)";
return [h,s,l]; return [h, s, l];
}, },
hue: 0, hue: 0,
saturation: 0, saturation: 0,
lightness: 0, lightness: 0,
changeElementColor: (element: HTMLElement | null, hue: number, saturation: number, lightness: number) => { changeElementColor: (
element: HTMLElement | null,
hue: number,
saturation: number,
lightness: number,
) => {
if (element) { if (element) {
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`; element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
} }
@ -84,19 +90,32 @@ export default createPlugin({
start() { start() {
this.playerPage = document.querySelector<HTMLElement>('#player-page'); this.playerPage = document.querySelector<HTMLElement>('#player-page');
this.navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background'); this.navBarBackground = document.querySelector<HTMLElement>(
this.ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar'); '#nav-bar-background',
this.playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background'); );
this.ytmusicPlayerBar =
document.querySelector<HTMLElement>('ytmusic-player-bar');
this.playerBarBackground = document.querySelector<HTMLElement>(
'#player-bar-background',
);
this.sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper'); this.sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
this.sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background'); this.sidebarSmall = document.querySelector<HTMLElement>(
'#mini-guide-background',
);
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
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') {
const isPageOpen = this.ytmusicAppLayout?.hasAttribute('player-page-open'); const isPageOpen =
this.ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) { if (isPageOpen) {
this.changeElementColor(this.sidebarSmall, this.hue, this.saturation, this.lightness - 30); this.changeElementColor(
this.sidebarSmall,
this.hue,
this.saturation,
this.lightness - 30,
);
} else { } else {
if (this.sidebarSmall) { if (this.sidebarSmall) {
this.sidebarSmall.style.backgroundColor = 'black'; this.sidebarSmall.style.backgroundColor = 'black';
@ -113,35 +132,84 @@ export default createPlugin({
onPlayerApiReady(playerApi) { onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor(); const fastAverageColor = new FastAverageColor();
document.addEventListener('videodatachange', (event: CustomEvent<VideoDataChanged>) => { document.addEventListener(
if (event.detail.name === 'dataloaded') { 'videodatachange',
const playerResponse = playerApi.getPlayerResponse(); (event: CustomEvent<VideoDataChanged>) => {
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0); if (event.detail.name === 'dataloaded') {
if (thumbnail) { const playerResponse = playerApi.getPlayerResponse();
fastAverageColor.getColorAsync(thumbnail.url) const thumbnail =
.then((albumColor) => { playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (albumColor) { if (thumbnail) {
const [hue, saturation, lightness] = [this.hue, this.saturation, this.lightness] = this.hexToHSL(albumColor.hex); fastAverageColor
this.changeElementColor(this.playerPage, hue, saturation, lightness - 30); .getColorAsync(thumbnail.url)
this.changeElementColor(this.navBarBackground, hue, saturation, lightness - 15); .then((albumColor) => {
this.changeElementColor(this.ytmusicPlayerBar, hue, saturation, lightness - 15); if (albumColor) {
this.changeElementColor(this.playerBarBackground, hue, saturation, lightness - 15); const [hue, saturation, lightness] = ([
this.changeElementColor(this.sidebarBig, hue, saturation, lightness - 15); this.hue,
if (this.ytmusicAppLayout?.hasAttribute('player-page-open')) { this.saturation,
this.changeElementColor(this.sidebarSmall, hue, saturation, lightness - 30); this.lightness,
] = this.hexToHSL(albumColor.hex));
this.changeElementColor(
this.playerPage,
hue,
saturation,
lightness - 30,
);
this.changeElementColor(
this.navBarBackground,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.ytmusicPlayerBar,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.playerBarBackground,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.sidebarBig,
hue,
saturation,
lightness - 15,
);
if (
this.ytmusicAppLayout?.hasAttribute('player-page-open')
) {
this.changeElementColor(
this.sidebarSmall,
hue,
saturation,
lightness - 30,
);
}
const ytRightClickList =
document.querySelector<HTMLElement>(
'tp-yt-paper-listbox',
);
this.changeElementColor(
ytRightClickList,
hue,
saturation,
lightness - 15,
);
} else {
if (this.playerPage) {
this.playerPage.style.backgroundColor = '#000000';
}
} }
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox'); })
this.changeElementColor(ytRightClickList, hue, saturation, lightness - 15); .catch((e) => console.error(e));
} else { }
if (this.playerPage) {
this.playerPage.style.backgroundColor = '#000000';
}
}
})
.catch((e) => console.error(e));
} }
} },
}); );
}, },
} },
}); });

View File

@ -4,23 +4,33 @@ yt-page-navigation-progress {
} }
#player-page { #player-page {
transition: transform 300ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important; transition:
transform 300ms,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#nav-bar-background { #nav-bar-background {
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important; transition:
opacity 200ms,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#mini-guide-background { #mini-guide-background {
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important; transition:
opacity 200ms,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
border-right: 0px !important; border-right: 0px !important;
} }
#guide-wrapper { #guide-wrapper {
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important; transition:
opacity 200ms,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#img, #player, .song-media-controls.style-scope.ytmusic-player { #img,
#player,
.song-media-controls.style-scope.ytmusic-player {
border-radius: 2% !important; border-radius: 2% !important;
} }

View File

@ -25,7 +25,8 @@ const defaultConfig: AmbientModePluginConfig = {
export default createPlugin({ export default createPlugin({
name: 'Ambient Mode', name: 'Ambient Mode',
description: 'Applies a lighting effect by casting gentle colors from the video, into your screens background.', description:
'Applies a lighting effect by casting gentle colors from the video, into your screens background.',
restartNeeded: false, restartNeeded: false,
config: defaultConfig, config: defaultConfig,
stylesheets: [style], stylesheets: [style],
@ -133,7 +134,9 @@ export default createPlugin({
start() { start() {
const injectBlurVideo = (): (() => void) | null => { const injectBlurVideo = (): (() => void) | null => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video'); const songVideo = document.querySelector<HTMLDivElement>('#song-video');
const video = document.querySelector<HTMLVideoElement>('#song-video .html5-video-container > video'); const video = document.querySelector<HTMLVideoElement>(
'#song-video .html5-video-container > video',
);
const wrapper = document.querySelector('#song-video > .player-wrapper'); const wrapper = document.querySelector('#song-video > .player-wrapper');
if (!songVideo) return null; if (!songVideo) return null;
@ -143,27 +146,34 @@ export default createPlugin({
const blurCanvas = document.createElement('canvas'); const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas'); blurCanvas.classList.add('html5-blur-canvas');
const context = blurCanvas.getContext('2d', { willReadFrequently: true }); const context = blurCanvas.getContext('2d', {
willReadFrequently: true,
});
/* effect */ /* effect */
let lastEffectWorkId: number | null = null; let lastEffectWorkId: number | null = null;
let lastImageData: ImageData | null = null; let lastImageData: ImageData | null = null;
const onSync = () => { const onSync = () => {
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId); if (typeof lastEffectWorkId === 'number')
cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => { lastEffectWorkId = requestAnimationFrame(() => {
// console.log('context', context); // console.log('context', context);
if (!context) return; if (!context) return;
const width = this.qualityRatio; const width = this.qualityRatio;
let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1); let height = Math.max(
Math.floor((blurCanvas.height / blurCanvas.width) * width),
1,
);
if (!Number.isFinite(height)) height = width; if (!Number.isFinite(height)) height = width;
if (!height) return; if (!height) return;
context.globalAlpha = 1; context.globalAlpha = 1;
if (lastImageData) { if (lastImageData) {
const frameOffset = (1 / this.buffer) * (1000 / this.interpolationTime); const frameOffset =
(1 / this.buffer) * (1000 / this.interpolationTime);
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1 context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
context.putImageData(lastImageData, 0, 0); context.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset; context.globalAlpha = frameOffset;
@ -185,15 +195,17 @@ export default createPlugin({
if (newWidth === 0 || newHeight === 0) return; if (newWidth === 0 || newHeight === 0) return;
blurCanvas.width = this.qualityRatio; blurCanvas.width = this.qualityRatio;
blurCanvas.height = Math.floor(newHeight / newWidth * this.qualityRatio); blurCanvas.height = Math.floor(
(newHeight / newWidth) * this.qualityRatio,
);
blurCanvas.style.width = `${newWidth * this.sizeRatio}px`; blurCanvas.style.width = `${newWidth * this.sizeRatio}px`;
blurCanvas.style.height = `${newHeight * this.sizeRatio}px`; blurCanvas.style.height = `${newHeight * this.sizeRatio}px`;
if (this.isFullscreen) blurCanvas.classList.add('fullscreen'); if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen'); else blurCanvas.classList.remove('fullscreen');
const leftOffset = newWidth * (this.sizeRatio - 1) / 2; const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
const topOffset = newHeight * (this.sizeRatio - 1) / 2; const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`); blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`); blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
blurCanvas.style.setProperty('--blur', `${this.blur}px`); blurCanvas.style.setProperty('--blur', `${this.blur}px`);
@ -214,7 +226,10 @@ export default createPlugin({
/* hooking */ /* hooking */
let canvasInterval: NodeJS.Timeout | null = null; let canvasInterval: NodeJS.Timeout | null = null;
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer))); canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
applyVideoAttributes(); applyVideoAttributes();
observer.observe(songVideo, { attributes: true }); observer.observe(songVideo, { attributes: true });
resizeObserver.observe(songVideo); resizeObserver.observe(songVideo);
@ -226,7 +241,10 @@ export default createPlugin({
}; };
const onPlay = () => { const onPlay = () => {
if (canvasInterval) clearInterval(canvasInterval); if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer))); canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
}; };
songVideo.addEventListener('pause', onPause); songVideo.addEventListener('pause', onPause);
songVideo.addEventListener('play', onPlay); songVideo.addEventListener('play', onPlay);
@ -249,7 +267,6 @@ export default createPlugin({
}; };
}; };
const playerPage = document.querySelector<HTMLElement>('#player-page'); const playerPage = document.querySelector<HTMLElement>('#player-page');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
@ -262,7 +279,8 @@ export default createPlugin({
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') {
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open'); const isPageOpen =
ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) { if (isPageOpen) {
this.unregister?.(); this.unregister?.();
this.unregister = injectBlurVideo() ?? null; this.unregister = injectBlurVideo() ?? null;
@ -293,6 +311,6 @@ export default createPlugin({
this.observer?.disconnect(); this.observer?.disconnect();
this.update = null; this.update = null;
this.unregister?.(); this.unregister?.();
} },
} },
}); });

View File

@ -2,7 +2,8 @@ import { createPlugin } from '@/utils';
export default createPlugin({ export default createPlugin({
name: 'Audio Compressor', name: 'Audio Compressor',
description: 'Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)', description:
'Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)',
renderer() { renderer() {
document.addEventListener( document.addEventListener(

View File

@ -2,7 +2,7 @@ import { createPlugin } from '@/utils';
export default createPlugin({ export default createPlugin({
name: 'Bypass Age Restrictions', name: 'Bypass Age Restrictions',
description: 'bypass YouTube\'s age verification', description: "bypass YouTube's age verification",
restartNeeded: true, restartNeeded: true,
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript

View File

@ -1,16 +1,25 @@
<tp-yt-paper-icon-button aria-disabled="false" aria-label="Open captions selector" <tp-yt-paper-icon-button
class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles" aria-disabled="false"
role="button" tabindex="0" aria-label="Open captions selector"
title="Open captions selector"> class="player-captions-button style-scope ytmusic-player"
icon="yt-icons:subtitles"
role="button"
tabindex="0"
title="Open captions selector"
>
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon"> <tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
<svg class="style-scope yt-icon" <svg
focusable="false" preserveAspectRatio="xMidYMid meet" class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;" focusable="false"
viewBox="0 0 24 24"> preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon"> <g class="style-scope yt-icon">
<path <path
class="style-scope tp-yt-iron-icon" class="style-scope tp-yt-iron-icon"
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"></path> d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
></path>
</g> </g>
</svg> </svg>
</tp-yt-iron-icon> </tp-yt-iron-icon>

View File

@ -18,7 +18,10 @@ export default createPlugin<
getCompactSidebar: () => document.querySelector('#mini-guide'), getCompactSidebar: () => document.querySelector('#mini-guide'),
isCompactSidebarDisabled() { isCompactSidebarDisabled() {
const compactSidebar = this.getCompactSidebar(); const compactSidebar = this.getCompactSidebar();
return compactSidebar === null || window.getComputedStyle(compactSidebar).display === 'none'; return (
compactSidebar === null ||
window.getComputedStyle(compactSidebar).display === 'none'
);
}, },
start() { start() {
if (this.isCompactSidebarDisabled()) { if (this.isCompactSidebarDisabled()) {
@ -34,6 +37,6 @@ export default createPlugin<
if (this.isCompactSidebarDisabled()) { if (this.isCompactSidebarDisabled()) {
document.querySelector<HTMLButtonElement>('#button')?.click(); document.querySelector<HTMLButtonElement>('#button')?.click();
} }
} },
}, },
}); });

View File

@ -20,14 +20,16 @@ const validateVolumeLevel = (value: number) => {
// Number between 0 and 1? // Number between 0 and 1?
if (!Number.isNaN(value) && value >= 0 && value <= 1) { if (!Number.isNaN(value) && value >= 0 && value <= 1) {
// Yup, that's fine // Yup, that's fine
} else { } else {
// Abort and throw an exception // Abort and throw an exception
throw new TypeError('Number between 0 and 1 expected as volume!'); throw new TypeError('Number between 0 and 1 expected as volume!');
} }
}; };
type VolumeLogger = <Params extends unknown[]>(message: string, ...args: Params) => void; type VolumeLogger = <Params extends unknown[]>(
message: string,
...args: Params
) => void;
interface VolumeFaderOptions { interface VolumeFaderOptions {
/** /**
* logging `function(stuff, …)` for execution information (default: no logging) * logging `function(stuff, …)` for execution information (default: no logging)
@ -71,7 +73,6 @@ export class VolumeFader {
private active: boolean = false; private active: boolean = false;
private fade: VolumeFade | undefined; private fade: VolumeFade | undefined;
/** /**
* VolumeFader Constructor * VolumeFader Constructor
* *
@ -119,17 +120,17 @@ export class VolumeFader {
// Default dynamic range? // Default dynamic range?
if ( if (
options.fadeScaling === undefined options.fadeScaling === undefined ||
|| options.fadeScaling === 'logarithmic' options.fadeScaling === 'logarithmic'
) { ) {
// Set default of 60 dB // Set default of 60 dB
dynamicRange = 3; dynamicRange = 3;
} }
// Custom dynamic range? // Custom dynamic range?
else if ( else if (
typeof options.fadeScaling === 'number' typeof options.fadeScaling === 'number' &&
&& !Number.isNaN(options.fadeScaling) !Number.isNaN(options.fadeScaling) &&
&& options.fadeScaling > 0 options.fadeScaling > 0
) { ) {
// Turn amplitude dB into a multiple of 10 power dB // Turn amplitude dB into a multiple of 10 power dB
dynamicRange = options.fadeScaling / 2 / 10; dynamicRange = options.fadeScaling / 2 / 10;
@ -151,13 +152,13 @@ export class VolumeFader {
}; };
// Log setting if not default // Log setting if not default
options.fadeScaling options.fadeScaling &&
&& this.logger this.logger &&
&& this.logger( this.logger(
'Using logarithmic fading with ' 'Using logarithmic fading with ' +
+ String(10 * dynamicRange) String(10 * dynamicRange) +
+ ' dB dynamic range.', ' dB dynamic range.',
); );
} }
// Set initial volume? // Set initial volume?
@ -169,10 +170,8 @@ export class VolumeFader {
this.media.volume = options.initialVolume; this.media.volume = options.initialVolume;
// Log setting // Log setting
this.logger this.logger &&
&& this.logger( this.logger('Set initial volume to ' + String(this.media.volume) + '.');
'Set initial volume to ' + String(this.media.volume) + '.',
);
} }
// Fade duration given? // Fade duration given?
@ -237,8 +236,8 @@ export class VolumeFader {
this.fadeDuration = fadeDuration; this.fadeDuration = fadeDuration;
// Log setting // Log setting
this.logger this.logger &&
&& this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.'); this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else { } else {
// Abort and throw an exception // Abort and throw an exception
throw new TypeError('Positive number expected as fade duration!'); throw new TypeError('Positive number expected as fade duration!');
@ -308,13 +307,14 @@ export class VolumeFader {
// Time left for fading? // Time left for fading?
if (now < this.fade.time.end) { if (now < this.fade.time.end) {
// Compute current fade progress // Compute current fade progress
const progress const progress =
= (now - this.fade.time.start) (now - this.fade.time.start) /
/ (this.fade.time.end - this.fade.time.start); (this.fade.time.end - this.fade.time.start);
// Compute current level on internal scale // Compute current level on internal scale
const level const level =
= (progress * (this.fade.volume.end - this.fade.volume.start)) + this.fade.volume.start; (progress * (this.fade.volume.end - this.fade.volume.start)) +
this.fade.volume.start;
// Map fade level to volume level and apply it to media element // Map fade level to volume level and apply it to media element
this.media.volume = this.scale.internalToVolume(level); this.media.volume = this.scale.internalToVolume(level);
@ -323,10 +323,8 @@ export class VolumeFader {
window.requestAnimationFrame(this.updateVolume.bind(this)); window.requestAnimationFrame(this.updateVolume.bind(this));
} else { } else {
// Log end of fade // Log end of fade
this.logger this.logger &&
&& this.logger( this.logger('Fade to ' + String(this.fade.volume.end) + ' complete.');
'Fade to ' + String(this.fade.volume.end) + ' complete.',
);
// Time is up, jump to target volume // Time is up, jump to target volume
this.media.volume = this.scale.internalToVolume(this.fade.volume.end); this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
@ -389,5 +387,5 @@ export class VolumeFader {
} }
export default { export default {
VolumeFader VolumeFader,
}; };

View File

@ -18,7 +18,7 @@ export type CrossfadePluginConfig = {
fadeOutDuration: number; fadeOutDuration: number;
secondsBeforeEnd: number; secondsBeforeEnd: number;
fadeScaling: 'linear' | 'logarithmic' | number; fadeScaling: 'linear' | 'logarithmic' | number;
} };
export default createPlugin< export default createPlugin<
unknown, unknown,
@ -61,7 +61,10 @@ export default createPlugin<
fadeScaling: 'linear', fadeScaling: 'linear',
}, },
menu({ window, getConfig, setConfig }) { menu({ window, getConfig, setConfig }) {
const promptCrossfadeValues = async (win: BrowserWindow, options: CrossfadePluginConfig): Promise<Omit<CrossfadePluginConfig, 'enabled'> | undefined> => { const promptCrossfadeValues = async (
win: BrowserWindow,
options: CrossfadePluginConfig,
): Promise<Omit<CrossfadePluginConfig, 'enabled'> | undefined> => {
const res = await prompt( const res = await prompt(
{ {
title: 'Crossfade Options', title: 'Crossfade Options',
@ -89,8 +92,7 @@ export default createPlugin<
}, },
{ {
label: 'Crossfade x seconds before end', label: 'Crossfade x seconds before end',
value: value: options.secondsBeforeEnd,
options.secondsBeforeEnd,
inputAttrs: { inputAttrs: {
type: 'number', type: 'number',
required: true, required: true,
@ -135,7 +137,10 @@ export default createPlugin<
{ {
label: 'Advanced', label: 'Advanced',
async click() { async click() {
const newOptions = await promptCrossfadeValues(window, await getConfig()); const newOptions = await promptCrossfadeValues(
window,
await getConfig(),
);
if (newOptions) { if (newOptions) {
setConfig(newOptions); setConfig(newOptions);
} }
@ -170,11 +175,14 @@ export default createPlugin<
let firstVideo = true; let firstVideo = true;
let waitForTransition: Promise<unknown>; let waitForTransition: Promise<unknown>;
const getStreamURL = async (videoID: string): Promise<string> => this.ipc?.invoke('audio-url', videoID); const getStreamURL = async (videoID: string): Promise<string> =>
this.ipc?.invoke('audio-url', videoID);
const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); const getVideoIDFromURL = (url: string) =>
new URLSearchParams(url.split('?')?.at(-1)).get('v');
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded'; const isReadyToCrossfade = () =>
transitionAudio && transitionAudio.state() === 'loaded';
const watchVideoIDChanges = (cb: (id: string) => void) => { const watchVideoIDChanges = (cb: (id: string) => void) => {
window.navigation.addEventListener('navigate', (event) => { window.navigation.addEventListener('navigate', (event) => {
@ -184,9 +192,9 @@ export default createPlugin<
const nextVideoID = getVideoIDFromURL(event.destination.url ?? ''); const nextVideoID = getVideoIDFromURL(event.destination.url ?? '');
if ( if (
nextVideoID nextVideoID &&
&& currentVideoID currentVideoID &&
&& (firstVideo || nextVideoID !== currentVideoID) (firstVideo || nextVideoID !== currentVideoID)
) { ) {
if (isReadyToCrossfade()) { if (isReadyToCrossfade()) {
crossfade(() => { crossfade(() => {
@ -245,8 +253,9 @@ export default createPlugin<
// Exit just before the end for the transition // Exit just before the end for the transition
const transitionBeforeEnd = () => { const transitionBeforeEnd = () => {
if ( if (
video.currentTime >= video.duration - this.config!.secondsBeforeEnd video.currentTime >=
&& isReadyToCrossfade() video.duration - this.config!.secondsBeforeEnd &&
isReadyToCrossfade()
) { ) {
video.removeEventListener('timeupdate', transitionBeforeEnd); video.removeEventListener('timeupdate', transitionBeforeEnd);
@ -294,6 +303,6 @@ export default createPlugin<
createAudioForCrossfade(url); createAudioForCrossfade(url);
}); });
} },
} },
}); });

View File

@ -6,7 +6,7 @@ import type { YoutubePlayer } from '@/types/youtube-player';
export type DisableAutoPlayPluginConfig = { export type DisableAutoPlayPluginConfig = {
enabled: boolean; enabled: boolean;
applyOnce: boolean; applyOnce: boolean;
} };
export default createPlugin< export default createPlugin<
unknown, unknown,
@ -53,7 +53,11 @@ export default createPlugin<
if (event.detail.name === 'dataloaded') { if (event.detail.name === 'dataloaded') {
this.api?.pauseVideo(); this.api?.pauseVideo();
document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', this.timeUpdateListener, { once: true }); document
.querySelector<HTMLVideoElement>('video')
?.addEventListener('timeupdate', this.timeUpdateListener, {
once: true,
});
} }
}, },
timeUpdateListener(e: Event) { timeUpdateListener(e: Event) {
@ -74,7 +78,6 @@ export default createPlugin<
}, },
onConfigChange(newConfig) { onConfigChange(newConfig) {
this.config = newConfig; this.config = newConfig;
} },
} },
}); });

View File

@ -32,7 +32,7 @@ export type DiscordPluginConfig = {
* Hide the "duration left" in the rich presence * Hide the "duration left" in the rich presence
*/ */
hideDurationLeft: boolean; hideDurationLeft: boolean;
} };
export default createPlugin({ export default createPlugin({
name: 'Discord Rich Presence', name: 'Discord Rich Presence',
@ -50,4 +50,3 @@ export default createPlugin({
menu: onMenu, menu: onMenu,
backend, backend,
}); });

View File

@ -10,7 +10,6 @@ import { createBackend } from '@/utils';
import type { DiscordPluginConfig } from './index'; import type { DiscordPluginConfig } from './index';
// Application ID registered by @th-ch/youtube-music dev team // Application ID registered by @th-ch/youtube-music dev team
const clientId = '1177081335727267940'; const clientId = '1177081335727267940';
@ -47,13 +46,16 @@ const resetInfo = () => {
} }
}; };
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => { const connectTimeout = () =>
if (!info.autoReconnect || info.rpc.isConnected) { new Promise((resolve, reject) =>
return; setTimeout(() => {
} if (!info.autoReconnect || info.rpc.isConnected) {
return;
}
info.rpc.login().then(resolve).catch(reject); info.rpc.login().then(resolve).catch(reject);
}, 5000)); }, 5000),
);
const connectRecursive = () => { const connectRecursive = () => {
if (!info.autoReconnect || info.rpc.isConnected) { if (!info.autoReconnect || info.rpc.isConnected) {
return; return;
@ -106,10 +108,13 @@ export const clear = () => {
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb); export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
export const isConnected = () => info.rpc !== null; export const isConnected = () => info.rpc !== null;
export const backend = createBackend<{ export const backend = createBackend<
config?: DiscordPluginConfig; {
updateActivity: (songInfo: SongInfo, config: DiscordPluginConfig) => void; config?: DiscordPluginConfig;
}, DiscordPluginConfig>({ updateActivity: (songInfo: SongInfo, config: DiscordPluginConfig) => void;
},
DiscordPluginConfig
>({
/** /**
* We get multiple events * We get multiple events
* Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) * Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
@ -132,7 +137,11 @@ export const backend = createBackend<{
} }
// Clear directly if timeout is 0 // Clear directly if timeout is 0
if (songInfo.isPaused && config.activityTimeoutEnabled && config.activityTimeoutTime === 0) { if (
songInfo.isPaused &&
config.activityTimeoutEnabled &&
config.activityTimeoutTime === 0
) {
info.rpc.user?.clearActivity().catch(console.error); info.rpc.user?.clearActivity().catch(console.error);
return; return;
} }
@ -142,10 +151,14 @@ export const backend = createBackend<{
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530 // not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
if (songInfo.title.length < 2) { if (songInfo.title.length < 2) {
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length); songInfo.title += hangulFillerUnicodeCharacter.repeat(
2 - songInfo.title.length,
);
} }
if (songInfo.artist.length < 2) { if (songInfo.artist.length < 2) {
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length); songInfo.artist += hangulFillerUnicodeCharacter.repeat(
2 - songInfo.title.length,
);
} }
const activityInfo: SetActivity = { const activityInfo: SetActivity = {
@ -154,11 +167,17 @@ export const backend = createBackend<{
largeImageKey: songInfo.imageSrc ?? '', largeImageKey: songInfo.imageSrc ?? '',
largeImageText: songInfo.album ?? '', largeImageText: songInfo.album ?? '',
buttons: [ buttons: [
...(config.playOnYouTubeMusic ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }] : []), ...(config.playOnYouTubeMusic
...(config.hideGitHubButton ? [] : [{ ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }]
label: 'View App On GitHub', : []),
url: 'https://github.com/th-ch/youtube-music' ...(config.hideGitHubButton
}]), ? []
: [
{
label: 'View App On GitHub',
url: 'https://github.com/th-ch/youtube-music',
},
]),
], ],
}; };
@ -168,14 +187,16 @@ export const backend = createBackend<{
activityInfo.smallImageText = 'Paused'; activityInfo.smallImageText = 'Paused';
// Set start the timer so the activity gets cleared after a while if enabled // Set start the timer so the activity gets cleared after a while if enabled
if (config.activityTimeoutEnabled) { if (config.activityTimeoutEnabled) {
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), config.activityTimeoutTime ?? 10_000); clearActivity = setTimeout(
() => info.rpc.user?.clearActivity().catch(console.error),
config.activityTimeoutTime ?? 10_000,
);
} }
} else if (!config.hideDurationLeft) { } else if (!config.hideDurationLeft) {
// Add the start and end time of the song // Add the start and end time of the song
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000); const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
activityInfo.startTimestamp = songStartTime; activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp activityInfo.endTimestamp = songStartTime + (songInfo.songDuration * 1000);
= songStartTime + (songInfo.songDuration * 1000);
} }
info.rpc.user?.setActivity(activityInfo).catch(console.error); info.rpc.user?.setActivity(activityInfo).catch(console.error);

View File

@ -15,7 +15,12 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
registerRefresh(refreshMenu); registerRefresh(refreshMenu);
}); });
export const onMenu = async ({ window, getConfig, setConfig, refresh }: MenuContext<DiscordPluginConfig>): Promise<MenuTemplate> => { export const onMenu = async ({
window,
getConfig,
setConfig,
refresh,
}: MenuContext<DiscordPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
registerRefreshOnce(refresh); registerRefreshOnce(refresh);
@ -86,16 +91,22 @@ export const onMenu = async ({ window, getConfig, setConfig, refresh }: MenuCont
]; ];
}; };
async function setInactivityTimeout(win: Electron.BrowserWindow, options: DiscordPluginConfig) { async function setInactivityTimeout(
const output = await prompt({ win: Electron.BrowserWindow,
title: 'Set Inactivity Timeout', options: DiscordPluginConfig,
label: 'Enter inactivity timeout in seconds:', ) {
value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)), const output = await prompt(
type: 'counter', {
counterOptions: { minimum: 0, multiFire: true }, title: 'Set Inactivity Timeout',
width: 450, label: 'Enter inactivity timeout in seconds:',
...promptOptions(), value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)),
}, win); type: 'counter',
counterOptions: { minimum: 0, multiFire: true },
width: 450,
...promptOptions(),
},
win,
);
if (output) { if (output) {
options.activityTimeoutTime = Math.round(~~output * 1e3); options.activityTimeoutTime = Math.round(~~output * 1e3);

View File

@ -13,7 +13,7 @@ export type DownloaderPluginConfig = {
customPresetSetting: Preset; customPresetSetting: Preset;
skipExisting: boolean; skipExisting: boolean;
playlistMaxItems?: number; playlistMaxItems?: number;
} };
export const defaultConfig: DownloaderPluginConfig = { export const defaultConfig: DownloaderPluginConfig = {
enabled: false, enabled: false,
@ -37,6 +37,5 @@ export default createPlugin({
renderer: { renderer: {
start: onRendererLoad, start: onRendererLoad,
onPlayerApiReady, onPlayerApiReady,
} },
}); });

View File

@ -93,7 +93,11 @@ export const getCookieFromWindow = async (win: BrowserWindow) => {
let config: DownloaderPluginConfig; let config: DownloaderPluginConfig;
export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext<DownloaderPluginConfig>) => { export const onMainLoad = async ({
window: _win,
getConfig,
ipc,
}: BackendContext<DownloaderPluginConfig>) => {
win = _win; win = _win;
config = await getConfig(); config = await getConfig();
@ -107,7 +111,9 @@ export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContex
ipc.on('video-src-changed', (data: GetPlayerResponse) => { ipc.on('video-src-changed', (data: GetPlayerResponse) => {
playingUrl = data.microformat.microformatDataRenderer.urlCanonical; playingUrl = data.microformat.microformatDataRenderer.urlCanonical;
}); });
ipc.handle('download-playlist-request', async (url: string) => downloadPlaylist(url)); ipc.handle('download-playlist-request', async (url: string) =>
downloadPlaylist(url),
);
}; };
export const onConfigChange = (newConfig: DownloaderPluginConfig) => { export const onConfigChange = (newConfig: DownloaderPluginConfig) => {
@ -230,8 +236,7 @@ async function downloadSongUnsafe(
const selectedPreset = config.selectedPreset ?? 'mp3 (256kbps)'; const selectedPreset = config.selectedPreset ?? 'mp3 (256kbps)';
let presetSetting: Preset; let presetSetting: Preset;
if (selectedPreset === 'Custom') { if (selectedPreset === 'Custom') {
presetSetting = presetSetting = config.customPresetSetting ?? DefaultPresetList['Custom'];
config.customPresetSetting ?? DefaultPresetList['Custom'];
} else if (selectedPreset === 'Source') { } else if (selectedPreset === 'Source') {
presetSetting = DefaultPresetList['Source']; presetSetting = DefaultPresetList['Source'];
} else { } else {
@ -444,8 +449,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
} }
const playlistId = const playlistId =
getPlaylistID(givenUrl) || getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl));
getPlaylistID(new URL(playingUrl));
if (!playlistId) { if (!playlistId) {
sendError(new Error('No playlist ID found')); sendError(new Error('No playlist ID found'));

View File

@ -1,7 +1,8 @@
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow } from 'electron';
import is from 'electron-is'; import is from 'electron-is';
export const getFolder = (customFolder: string) => customFolder || app.getPath('downloads'); export const getFolder = (customFolder: string) =>
customFolder || app.getPath('downloads');
export const defaultMenuDownloadLabel = 'Download playlist'; export const defaultMenuDownloadLabel = 'Download playlist';
export const sendFeedback = (win: BrowserWindow, message?: unknown) => { export const sendFeedback = (win: BrowserWindow, message?: unknown) => {

View File

@ -9,7 +9,10 @@ import type { MenuTemplate } from '@/menu';
import type { DownloaderPluginConfig } from './index'; import type { DownloaderPluginConfig } from './index';
export const onMenu = async ({ getConfig, setConfig }: MenuContext<DownloaderPluginConfig>): Promise<MenuTemplate> => { export const onMenu = async ({
getConfig,
setConfig,
}: MenuContext<DownloaderPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
return [ return [

View File

@ -28,7 +28,9 @@ const menuObserver = new MutationObserver(() => {
return; return;
} }
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href; const menuUrl = document.querySelector<HTMLAnchorElement>(
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
)?.href;
if (!menuUrl?.includes('watch?') && doneFirstLoad) { if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return; return;
} }
@ -40,14 +42,18 @@ const menuObserver = new MutationObserver(() => {
return; return;
} }
setTimeout(() => doneFirstLoad ||= true, 500); setTimeout(() => (doneFirstLoad ||= true), 500);
}); });
export const onRendererLoad = ({ ipc }: RendererContext<DownloaderPluginConfig>) => { export const onRendererLoad = ({
ipc,
}: RendererContext<DownloaderPluginConfig>) => {
window.download = () => { window.download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint') ?.querySelector(
'ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint',
)
?.getAttribute('href'); ?.getAttribute('href');
if (videoUrl) { if (videoUrl) {
if (videoUrl.startsWith('watch?')) { if (videoUrl.startsWith('watch?')) {

View File

@ -16,7 +16,7 @@ export const DefaultPresetList: Record<string, Preset> = {
'Custom': { 'Custom': {
extension: null, extension: null,
ffmpegArgs: [], ffmpegArgs: [],
} },
}; };
export interface YouTubeFormat { export interface YouTubeFormat {
@ -31,86 +31,742 @@ export interface YouTubeFormat {
// converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md // converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md
export const YoutubeFormatList: YouTubeFormat[] = [ export const YoutubeFormatList: YouTubeFormat[] = [
{ itag: 5, container: 'flv', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '-' }, {
{ itag: 6, container: 'flv', content: 'audio/video', resolution: '270p', bitrate: '-', range: '-', vrOr3D: '-' }, itag: 5,
{ itag: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' }, container: 'flv',
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' }, content: 'audio/video',
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' }, resolution: '240p',
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' }, bitrate: '-',
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' }, range: '-',
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', vrOr3D: '-' }, vrOr3D: '-',
{ itag: 37, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' }, },
{ itag: 38, container: 'mp4', content: 'audio/video', resolution: '3072p', bitrate: '-', range: '-', vrOr3D: '-' }, {
{ itag: 43, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' }, itag: 6,
{ itag: 44, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' }, container: 'flv',
{ itag: 45, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' }, content: 'audio/video',
{ itag: 46, container: 'webm', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' }, resolution: '270p',
{ itag: 82, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' }, bitrate: '-',
{ itag: 83, container: 'mp4', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' }, range: '-',
{ itag: 84, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' }, vrOr3D: '-',
{ itag: 85, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '3D' }, },
{ itag: 91, container: 'hls', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '3D' }, {
{ itag: 92, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '3D' }, itag: 17,
{ itag: 93, container: 'hls', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' }, container: '3gp',
{ itag: 94, container: 'hls', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' }, content: 'audio/video',
{ itag: 95, container: 'hls', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' }, resolution: '144p',
{ itag: 96, container: 'hls', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' }, bitrate: '-',
{ itag: 100, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' }, range: '-',
{ itag: 101, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' }, vrOr3D: '-',
{ itag: 102, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' }, },
{ itag: 132, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' }, {
{ itag: 133, container: 'mp4', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' }, itag: 18,
{ itag: 134, container: 'mp4', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' }, container: 'mp4',
{ itag: 135, container: 'mp4', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 136, container: 'mp4', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' }, resolution: '360p',
{ itag: 137, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' }, bitrate: '-',
{ itag: 138, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' }, range: '-',
{ itag: 139, container: 'm4a', content: 'audio', resolution: '-', bitrate: '48k', range: '-', vrOr3D: '' }, vrOr3D: '-',
{ itag: 140, container: 'm4a', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' }, },
{ itag: 141, container: 'm4a', content: 'audio', resolution: '-', bitrate: '256k', range: '-', vrOr3D: '' }, {
{ itag: 151, container: 'hls', content: 'audio/video', resolution: '72p', bitrate: '-', range: '-', vrOr3D: '' }, itag: 22,
{ itag: 160, container: 'mp4', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' }, container: 'mp4',
{ itag: 167, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 168, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, resolution: '720p',
{ itag: 169, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' }, bitrate: '-',
{ itag: 171, container: 'webm', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' }, range: '-',
{ itag: 218, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, vrOr3D: '-',
{ itag: 219, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' }, },
{ itag: 242, container: 'webm', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' }, {
{ itag: 243, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' }, itag: 34,
{ itag: 244, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, container: 'flv',
{ itag: 245, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 246, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' }, resolution: '360p',
{ itag: 247, container: 'webm', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' }, bitrate: '-',
{ itag: 248, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' }, range: '-',
{ itag: 249, container: 'webm', content: 'audio', resolution: '-', bitrate: '50k', range: '-', vrOr3D: '' }, vrOr3D: '-',
{ itag: 250, container: 'webm', content: 'audio', resolution: '-', bitrate: '70k', range: '-', vrOr3D: '' }, },
{ itag: 251, container: 'webm', content: 'audio', resolution: '-', bitrate: '160k', range: '-', vrOr3D: '' }, {
{ itag: 264, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' }, itag: 35,
{ itag: 266, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' }, container: 'flv',
{ itag: 271, container: 'webm', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 272, container: 'webm', content: 'video', resolution: '4320p', bitrate: '-', range: '-', vrOr3D: '' }, resolution: '480p',
{ itag: 278, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' }, bitrate: '-',
{ itag: 298, container: 'mp4', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' }, range: '-',
{ itag: 299, container: 'mp4', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' }, vrOr3D: '-',
{ itag: 302, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' }, },
{ itag: 303, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' }, {
{ itag: 308, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: '-', vrOr3D: '' }, itag: 36,
{ itag: 313, container: 'webm', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' }, container: '3gp',
{ itag: 315, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 330, container: 'webm', content: 'video', resolution: '144p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, resolution: '180p',
{ itag: 331, container: 'webm', content: 'video', resolution: '240p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, bitrate: '-',
{ itag: 332, container: 'webm', content: 'video', resolution: '360p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, range: '-',
{ itag: 333, container: 'webm', content: 'video', resolution: '480p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, vrOr3D: '-',
{ itag: 334, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, },
{ itag: 335, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, {
{ itag: 336, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, itag: 37,
{ itag: 337, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: 'hdr', vrOr3D: '' }, container: 'mp4',
{ itag: 272, container: 'webm', content: 'video', resolution: '2880p/4320p', bitrate: '-', range: '-', vrOr3D: '' }, content: 'audio/video',
{ itag: 399, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' }, resolution: '1080p',
{ itag: 400, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' }, bitrate: '-',
{ itag: 401, container: 'mp4', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' }, range: '-',
{ itag: 402, container: 'mp4', content: 'video', resolution: '2880p', bitrate: '-', range: '-', vrOr3D: '' }, vrOr3D: '-',
{ itag: 571, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' }, },
{ itag: 702, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' }, {
itag: 38,
container: 'mp4',
content: 'audio/video',
resolution: '3072p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 43,
container: 'webm',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 44,
container: 'webm',
content: 'audio/video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 45,
container: 'webm',
content: 'audio/video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 46,
container: 'webm',
content: 'audio/video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 82,
container: 'mp4',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 83,
container: 'mp4',
content: 'audio/video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 84,
container: 'mp4',
content: 'audio/video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 85,
container: 'mp4',
content: 'audio/video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 91,
container: 'hls',
content: 'audio/video',
resolution: '144p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 92,
container: 'hls',
content: 'audio/video',
resolution: '240p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 93,
container: 'hls',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 94,
container: 'hls',
content: 'audio/video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 95,
container: 'hls',
content: 'audio/video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 96,
container: 'hls',
content: 'audio/video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 100,
container: 'webm',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 101,
container: 'webm',
content: 'audio/video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 102,
container: 'webm',
content: 'audio/video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '3D',
},
{
itag: 132,
container: 'hls',
content: 'audio/video',
resolution: '240p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 133,
container: 'mp4',
content: 'video',
resolution: '240p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 134,
container: 'mp4',
content: 'video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 135,
container: 'mp4',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 136,
container: 'mp4',
content: 'video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 137,
container: 'mp4',
content: 'video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 138,
container: 'mp4',
content: 'video',
resolution: '2160p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 139,
container: 'm4a',
content: 'audio',
resolution: '-',
bitrate: '48k',
range: '-',
vrOr3D: '',
},
{
itag: 140,
container: 'm4a',
content: 'audio',
resolution: '-',
bitrate: '128k',
range: '-',
vrOr3D: '',
},
{
itag: 141,
container: 'm4a',
content: 'audio',
resolution: '-',
bitrate: '256k',
range: '-',
vrOr3D: '',
},
{
itag: 151,
container: 'hls',
content: 'audio/video',
resolution: '72p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 160,
container: 'mp4',
content: 'video',
resolution: '144p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 167,
container: 'webm',
content: 'video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 168,
container: 'webm',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 169,
container: 'webm',
content: 'video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 171,
container: 'webm',
content: 'audio',
resolution: '-',
bitrate: '128k',
range: '-',
vrOr3D: '',
},
{
itag: 218,
container: 'webm',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 219,
container: 'webm',
content: 'video',
resolution: '144p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 242,
container: 'webm',
content: 'video',
resolution: '240p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 243,
container: 'webm',
content: 'video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 244,
container: 'webm',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 245,
container: 'webm',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 246,
container: 'webm',
content: 'video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 247,
container: 'webm',
content: 'video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 248,
container: 'webm',
content: 'video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 249,
container: 'webm',
content: 'audio',
resolution: '-',
bitrate: '50k',
range: '-',
vrOr3D: '',
},
{
itag: 250,
container: 'webm',
content: 'audio',
resolution: '-',
bitrate: '70k',
range: '-',
vrOr3D: '',
},
{
itag: 251,
container: 'webm',
content: 'audio',
resolution: '-',
bitrate: '160k',
range: '-',
vrOr3D: '',
},
{
itag: 264,
container: 'mp4',
content: 'video',
resolution: '1440p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 266,
container: 'mp4',
content: 'video',
resolution: '2160p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 271,
container: 'webm',
content: 'video',
resolution: '1440p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 272,
container: 'webm',
content: 'video',
resolution: '4320p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 278,
container: 'webm',
content: 'video',
resolution: '144p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 298,
container: 'mp4',
content: 'video',
resolution: '720p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 299,
container: 'mp4',
content: 'video',
resolution: '1080p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 302,
container: 'webm',
content: 'video',
resolution: '720p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 303,
container: 'webm',
content: 'video',
resolution: '1080p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 308,
container: 'webm',
content: 'video',
resolution: '1440p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 313,
container: 'webm',
content: 'video',
resolution: '2160p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 315,
container: 'webm',
content: 'video',
resolution: '2160p60',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 330,
container: 'webm',
content: 'video',
resolution: '144p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 331,
container: 'webm',
content: 'video',
resolution: '240p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 332,
container: 'webm',
content: 'video',
resolution: '360p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 333,
container: 'webm',
content: 'video',
resolution: '480p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 334,
container: 'webm',
content: 'video',
resolution: '720p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 335,
container: 'webm',
content: 'video',
resolution: '1080p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 336,
container: 'webm',
content: 'video',
resolution: '1440p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 337,
container: 'webm',
content: 'video',
resolution: '2160p60',
bitrate: '-',
range: 'hdr',
vrOr3D: '',
},
{
itag: 272,
container: 'webm',
content: 'video',
resolution: '2880p/4320p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 399,
container: 'mp4',
content: 'video',
resolution: '1080p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 400,
container: 'mp4',
content: 'video',
resolution: '1440p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 401,
container: 'mp4',
content: 'video',
resolution: '2160p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 402,
container: 'mp4',
content: 'video',
resolution: '2880p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 571,
container: 'mp4',
content: 'video',
resolution: '3840p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
{
itag: 702,
container: 'mp4',
content: 'video',
resolution: '3840p',
bitrate: '-',
range: '-',
vrOr3D: '',
},
]; ];

View File

@ -2,7 +2,8 @@ import { createPlugin } from '@/utils';
export default createPlugin({ export default createPlugin({
name: 'Exponential Volume', name: 'Exponential Volume',
description: 'Makes the volume slider exponential so it\'s easier to select lower volumes.', description:
"Makes the volume slider exponential so it's easier to select lower volumes.",
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -24,7 +25,8 @@ export default createPlugin({
); );
Object.defineProperty(HTMLMediaElement.prototype, 'volume', { Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
get(this: HTMLMediaElement) { get(this: HTMLMediaElement) {
const lowVolume = propertyDescriptor?.get?.call(this) as number ?? 0; const lowVolume =
(propertyDescriptor?.get?.call(this) as number) ?? 0;
const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT); const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT);
// The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values. // The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values.
@ -46,6 +48,6 @@ export default createPlugin({
propertyDescriptor?.set?.call(this, lowVolume); propertyDescriptor?.set?.call(this, lowVolume);
}, },
}); });
} },
} },
}); });

View File

@ -13,13 +13,10 @@ export default createPlugin({
description: 'gives menu-bars a fancy, dark or album-color look', description: 'gives menu-bars a fancy, dark or album-color look',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: ( enabled:
typeof window !== 'undefined' && (typeof window !== 'undefined' &&
!window.navigator?.userAgent?.includes('mac') !window.navigator?.userAgent?.includes('mac')) ||
) || ( (typeof global !== 'undefined' && global.process?.platform !== 'darwin'),
typeof global !== 'undefined' &&
global.process?.platform !== 'darwin'
),
hideDOMWindowControls: false, hideDOMWindowControls: false,
} as InAppMenuConfig, } as InAppMenuConfig,
stylesheets: [titlebarStyle], stylesheets: [titlebarStyle],
@ -31,4 +28,3 @@ export default createPlugin({
onPlayerApiReady, onPlayerApiReady,
}, },
}); });

View File

@ -5,7 +5,10 @@ import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
import type { InAppMenuConfig } from './index'; import type { InAppMenuConfig } from './index';
export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContext<InAppMenuConfig>) => { export const onMainLoad = ({
window: win,
ipc: { handle, send },
}: BackendContext<InAppMenuConfig>) => {
win.on('close', () => { win.on('close', () => {
send('close-all-in-app-menu-panel'); send('close-all-in-app-menu-panel');
}); });
@ -16,11 +19,13 @@ export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContex
}); });
}); });
handle( handle('get-menu', () =>
'get-menu', JSON.parse(
() => JSON.parse(JSON.stringify( JSON.stringify(
Menu.getApplicationMenu(), Menu.getApplicationMenu(),
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), (key: string, value: unknown) =>
key !== 'commandsMap' && key !== 'menu' ? value : undefined,
),
), ),
); );
@ -28,7 +33,7 @@ export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContex
const menu = Menu.getApplicationMenu(); const menu = Menu.getApplicationMenu();
let target: MenuItem | null = null; let target: MenuItem | null = null;
const stack = [...menu?.items ?? []]; const stack = [...(menu?.items ?? [])];
while (stack.length > 0) { while (stack.length > 0) {
const now = stack.shift(); const now = stack.shift();
now?.submenu?.items.forEach((item) => stack.push(item)); now?.submenu?.items.forEach((item) => stack.push(item));
@ -44,15 +49,21 @@ export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContex
ipcMain.handle('menu-event', (event, commandId: number) => { ipcMain.handle('menu-event', (event, commandId: number) => {
const target = getMenuItemById(commandId); const target = getMenuItemById(commandId);
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender); if (target)
target.click(
undefined,
BrowserWindow.fromWebContents(event.sender),
event.sender,
);
}); });
handle('get-menu-by-id', (commandId: number) => { handle('get-menu-by-id', (commandId: number) => {
const result = getMenuItemById(commandId); const result = getMenuItemById(commandId);
return JSON.parse(JSON.stringify( return JSON.parse(
result, JSON.stringify(result, (key: string, value: unknown) =>
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), key !== 'commandsMap' && key !== 'menu' ? value : undefined,
),
); );
}); });

View File

@ -4,7 +4,10 @@ import type { InAppMenuConfig } from './index';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
export const onMenu = async ({ getConfig, setConfig }: MenuContext<InAppMenuConfig>): Promise<MenuTemplate> => { export const onMenu = async ({
getConfig,
setConfig,
}: MenuContext<InAppMenuConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
if (is.linux()) { if (is.linux()) {
@ -16,8 +19,8 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<InAppMenuConf
click(item) { click(item) {
config.hideDOMWindowControls = item.checked; config.hideDOMWindowControls = item.checked;
setConfig(config); setConfig(config);
} },
} },
]; ];
} }

View File

@ -1,9 +1,13 @@
const Icons = { const Icons = {
submenu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>', submenu:
checkbox: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
checkbox:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
radio: { radio: {
checked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>', checked:
unchecked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
unchecked:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
}, },
}; };

View File

@ -27,8 +27,12 @@ export const createPanel = (
if (item.checked) iconWrapper.innerHTML = Icons.radio.checked; if (item.checked) iconWrapper.innerHTML = Icons.radio.checked;
else iconWrapper.innerHTML = Icons.radio.unchecked; else iconWrapper.innerHTML = Icons.radio.unchecked;
} else { } else {
const iconURL = typeof item.icon === 'string' ? const iconURL =
await window.ipcRenderer.invoke('image-path-to-data-url') as string : item.icon?.toDataURL(); typeof item.icon === 'string'
? ((await window.ipcRenderer.invoke(
'image-path-to-data-url',
)) as string)
: item.icon?.toDataURL();
if (iconURL) iconWrapper.style.background = `url(${iconURL})`; if (iconURL) iconWrapper.style.background = `url(${iconURL})`;
} }
@ -36,7 +40,8 @@ export const createPanel = (
const radioGroups: [MenuItem, HTMLElement][] = []; const radioGroups: [MenuItem, HTMLElement][] = [];
items.map((item) => { items.map((item) => {
if (item.type === 'separator') return panel.appendChild(document.createElement('menu-separator')); if (item.type === 'separator')
return panel.appendChild(document.createElement('menu-separator'));
const menu = document.createElement('menu-item'); const menu = document.createElement('menu-item');
const iconWrapper = document.createElement('menu-icon'); const iconWrapper = document.createElement('menu-icon');
@ -47,7 +52,10 @@ export const createPanel = (
menu.addEventListener('click', async () => { menu.addEventListener('click', async () => {
await window.ipcRenderer.invoke('menu-event', item.commandId); await window.ipcRenderer.invoke('menu-event', item.commandId);
const menuItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; const menuItem = (await window.ipcRenderer.invoke(
'get-menu-by-id',
item.commandId,
)) as MenuItem | null;
if (menuItem) { if (menuItem) {
updateIconState(iconWrapper, menuItem); updateIconState(iconWrapper, menuItem);
@ -56,10 +64,13 @@ export const createPanel = (
await Promise.all( await Promise.all(
radioGroups.map(async ([item, iconWrapper]) => { radioGroups.map(async ([item, iconWrapper]) => {
if (item.commandId === menuItem.commandId) return; if (item.commandId === menuItem.commandId) return;
const newItem = await window.ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; const newItem = (await window.ipcRenderer.invoke(
'get-menu-by-id',
item.commandId,
)) as MenuItem | null;
if (newItem) updateIconState(iconWrapper, newItem); if (newItem) updateIconState(iconWrapper, newItem);
}) }),
); );
} }
} }
@ -74,10 +85,15 @@ export const createPanel = (
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu)); subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
menu.appendChild(subMenuIcon); menu.appendChild(subMenuIcon);
const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], { const [child, , children] = createPanel(
placement: 'right', parent,
order: (options?.order ?? 0) + 1, menu,
}); item.submenu?.items ?? [],
{
placement: 'right',
order: (options?.order ?? 0) + 1,
},
);
childPanels.push(child); childPanels.push(child);
children.push(...children); children.push(...children);
@ -106,7 +122,10 @@ export const createPanel = (
// long lists to squeeze their children at the bottom of the screen // long lists to squeeze their children at the bottom of the screen
// (This needs to be done *after* setAttribute) // (This needs to be done *after* setAttribute)
panel.classList.remove('position-by-bottom'); panel.classList.remove('position-by-bottom');
if (options.placement === 'right' && panel.scrollHeight > panel.clientHeight ) { if (
options.placement === 'right' &&
panel.scrollHeight > panel.clientHeight
) {
panel.style.setProperty('--y', `${rect.y + rect.height}px`); panel.style.setProperty('--y', `${rect.y + rect.height}px`);
panel.classList.add('position-by-bottom'); panel.classList.add('position-by-bottom');
} }
@ -119,16 +138,17 @@ export const createPanel = (
document.body.addEventListener('click', (event) => { document.body.addEventListener('click', (event) => {
const path = event.composedPath(); const path = event.composedPath();
const isInside = path.some((it) => it === panel || it === anchor || childPanels.includes(it as HTMLElement)); const isInside = path.some(
(it) =>
it === panel ||
it === anchor ||
childPanels.includes(it as HTMLElement),
);
if (!isInside) close(); if (!isInside) close();
}); });
parent.appendChild(panel); parent.appendChild(panel);
return [ return [panel, { isOpened, close, open }, childPanels] as const;
panel,
{ isOpened, close, open },
childPanels,
] as const;
}; };

View File

@ -12,9 +12,13 @@ import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '@/plugins/in-app-menu/index'; import type { InAppMenuConfig } from '@/plugins/in-app-menu/index';
const isMacOS = navigator.userAgent.includes('Macintosh'); const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; const isNotWindowsOrMacOS =
!navigator.userAgent.includes('Windows') && !isMacOS;
export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: RendererContext<InAppMenuConfig>) => { export const onRendererLoad = async ({
getConfig,
ipc: { invoke, on },
}: RendererContext<InAppMenuConfig>) => {
const config = await getConfig(); const config = await getConfig();
const hideDOMWindowControls = config.hideDOMWindowControls; const hideDOMWindowControls = config.hideDOMWindowControls;
@ -70,7 +74,6 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
titleBar.appendChild(logo); titleBar.appendChild(logo);
const addWindowControls = async () => { const addWindowControls = async () => {
// Create window control buttons // Create window control buttons
const minimizeButton = document.createElement('button'); const minimizeButton = document.createElement('button');
minimizeButton.classList.add('window-control'); minimizeButton.classList.add('window-control');
@ -124,12 +127,20 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
if (navBar) { if (navBar) {
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach(() => { mutations.forEach(() => {
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); titleBar.style.setProperty(
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); '--titlebar-background-color',
navBar.style.backgroundColor,
);
document
.querySelector('html')!
.style.setProperty(
'--titlebar-background-color',
navBar.style.backgroundColor,
);
}); });
}); });
observer.observe(navBar, { attributes : true, attributeFilter : ['style'] }); observer.observe(navBar, { attributes: true, attributeFilter: ['style'] });
} }
const updateMenu = async () => { const updateMenu = async () => {
@ -139,12 +150,16 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
}); });
panelClosers = []; panelClosers = [];
const menu = await invoke('get-menu') as Menu | null; const menu = (await invoke('get-menu')) as Menu | null;
if (!menu) return; if (!menu) return;
menu.items.forEach((menuItem) => { menu.items.forEach((menuItem) => {
const menu = document.createElement('menu-button'); const menu = document.createElement('menu-button');
const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []); const [, { close: closer }] = createPanel(
titleBar,
menu,
menuItem.submenu?.items ?? [],
);
panelClosers.push(closer); panelClosers.push(closer);
menu.append(menuItem.label); menu.append(menuItem.label);
@ -153,7 +168,8 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
menu.style.visibility = 'hidden'; menu.style.visibility = 'hidden';
} }
}); });
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); if (isNotWindowsOrMacOS && !hideDOMWindowControls)
await addWindowControls();
}; };
await updateMenu(); await updateMenu();
@ -164,13 +180,21 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
}); });
on('refresh-in-app-menu', () => updateMenu()); on('refresh-in-app-menu', () => updateMenu());
on('window-maximize', () => { on('window-maximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
} }
}); });
on('window-unmaximize', () => { on('window-unmaximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild); maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
} }
@ -187,6 +211,9 @@ export const onPlayerApiReady = () => {
const htmlHeadStyle = document.querySelector('head > div > style'); const htmlHeadStyle = document.querySelector('head > div > style');
if (htmlHeadStyle) { if (htmlHeadStyle) {
// HACK: This is a hack to remove the scrollbar width // HACK: This is a hack to remove the scrollbar width
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace(
'html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);',
'html::-webkit-scrollbar {',
);
} }
}; };

View File

@ -27,7 +27,9 @@ title-bar {
background-color: var(--titlebar-background-color, #030303); background-color: var(--titlebar-background-color, #030303);
user-select: none; user-select: none;
transition: opacity 200ms ease 0s, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s; transition:
opacity 200ms ease 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
} }
menu-button { menu-button {
@ -64,18 +66,26 @@ menu-panel {
padding: 4px; padding: 4px;
border-radius: 8px; border-radius: 8px;
pointer-events: none; pointer-events: none;
background-color: color-mix(in srgb, var(--titlebar-background-color, #030303) 50%, rgba(0, 0, 0, 0.1)); background-color: color-mix(
in srgb,
var(--titlebar-background-color, #030303) 50%,
rgba(0, 0, 0, 0.1)
);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 0; z-index: 0;
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
transform-origin: top left; transform-origin: top left;
transition: opacity 200ms ease 0s, transform 200ms ease 0s; transition:
opacity 200ms ease 0s,
transform 200ms ease 0s;
} }
menu-panel[open="true"] { menu-panel[open='true'] {
pointer-events: all; pointer-events: all;
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@ -159,22 +169,32 @@ ytmusic-app-layout {
margin-top: var(--menu-bar-height, 36px) !important; margin-top: var(--menu-bar-height, 36px) !important;
} }
ytmusic-app-layout>[slot=nav-bar], #nav-bar-background.ytmusic-app-layout { ytmusic-app-layout > [slot='nav-bar'],
#nav-bar-background.ytmusic-app-layout {
top: var(--menu-bar-height, 36px) !important; top: var(--menu-bar-height, 36px) !important;
} }
#nav-bar-divider.ytmusic-app-layout { #nav-bar-divider.ytmusic-app-layout {
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important; top: calc(
var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)
) !important;
} }
ytmusic-app[is-bauhaus-sidenav-enabled] #guide-spacer.ytmusic-app, ytmusic-app[is-bauhaus-sidenav-enabled] #guide-spacer.ytmusic-app,
ytmusic-app[is-bauhaus-sidenav-enabled] #mini-guide-spacer.ytmusic-app { ytmusic-app[is-bauhaus-sidenav-enabled] #mini-guide-spacer.ytmusic-app {
margin-top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important; margin-top: calc(
var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)
) !important;
} }
ytmusic-app-layout>[slot=player-page] { ytmusic-app-layout > [slot='player-page'] {
margin-top: var(--menu-bar-height); margin-top: var(--menu-bar-height);
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height) - var(--ytmusic-player-bar-height)) !important; height: calc(
100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height) -
var(--ytmusic-player-bar-height)
) !important;
} }
ytmusic-guide-renderer { ytmusic-guide-renderer {
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height)) !important; height: calc(
100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height)
) !important;
} }

View File

@ -63,13 +63,17 @@ export default createPlugin({
if (!songInfo.isPaused) { if (!songInfo.isPaused) {
setNowPlaying(songInfo, config, setConfig); setNowPlaying(songInfo, config, setConfig);
// Scrobble when the song is halfway through, or has passed the 4-minute mark // Scrobble when the song is halfway through, or has passed the 4-minute mark
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60); const scrobbleTime = Math.min(
Math.ceil(songInfo.songDuration / 2),
4 * 60,
);
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) { if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
// Scrobble still needs to happen // Scrobble still needs to happen
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000; const timeToWait =
(scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config); scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
} }
} }
}); });
} },
}); });

View File

@ -6,21 +6,21 @@ import type { LastFmPluginConfig } from './index';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
interface LastFmData { interface LastFmData {
method: string, method: string;
timestamp?: number, timestamp?: number;
} }
interface LastFmSongData { interface LastFmSongData {
track?: string, track?: string;
duration?: number, duration?: number;
artist?: string, artist?: string;
album?: string, album?: string;
api_key: string, api_key: string;
sk?: string, sk?: string;
format: string, format: string;
method: string, method: string;
timestamp?: number, timestamp?: number;
api_sig?: string, api_sig?: string;
} }
const createFormData = (parameters: LastFmSongData) => { const createFormData = (parameters: LastFmSongData) => {
@ -33,12 +33,19 @@ const createFormData = (parameters: LastFmSongData) => {
return formData; return formData;
}; };
const createQueryString = (parameters: Record<string, unknown>, apiSignature: string) => { const createQueryString = (
parameters: Record<string, unknown>,
apiSignature: string,
) => {
// Creates a querystring // Creates a querystring
const queryData = []; const queryData = [];
parameters.api_sig = apiSignature; parameters.api_sig = apiSignature;
for (const key in parameters) { for (const key in parameters) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(parameters[key]))}`); queryData.push(
`${encodeURIComponent(key)}=${encodeURIComponent(
String(parameters[key]),
)}`,
);
} }
return '?' + queryData.join('&'); return '?' + queryData.join('&');
@ -63,7 +70,11 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
return sig; return sig;
}; };
const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastFmPluginConfig) => { const createToken = async ({
api_key: apiKey,
api_root: apiRoot,
secret,
}: LastFmPluginConfig) => {
// Creates and stores the auth token // Creates and stores the auth token
const data = { const data = {
method: 'auth.gettoken', method: 'auth.gettoken',
@ -71,19 +82,28 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
format: 'json', format: 'json',
}; };
const apiSigature = createApiSig(data, secret); const apiSigature = createApiSig(data, secret);
const response = await net.fetch(`${apiRoot}${createQueryString(data, apiSigature)}`); const response = await net.fetch(
const json = await response.json() as Record<string, string>; `${apiRoot}${createQueryString(data, apiSigature)}`,
);
const json = (await response.json()) as Record<string, string>;
return json?.token; return json?.token;
}; };
const authenticate = async (config: LastFmPluginConfig) => { const authenticate = async (config: LastFmPluginConfig) => {
// Asks the user for authentication // Asks the user for authentication
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`); await shell.openExternal(
`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`,
);
}; };
type SetConfType = (conf: Partial<Omit<LastFmPluginConfig, 'enabled'>>) => (void | Promise<void>); type SetConfType = (
conf: Partial<Omit<LastFmPluginConfig, 'enabled'>>,
) => void | Promise<void>;
export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig: SetConfType) => { export const getAndSetSessionKey = async (
config: LastFmPluginConfig,
setConfig: SetConfType,
) => {
// Get and store the session key // Get and store the session key
const data = { const data = {
api_key: config.api_key, api_key: config.api_key,
@ -92,12 +112,14 @@ export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig:
token: config.token, token: config.token,
}; };
const apiSignature = createApiSig(data, config.secret); const apiSignature = createApiSig(data, config.secret);
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`); const response = await net.fetch(
const json = await response.json() as { `${config.api_root}${createQueryString(data, apiSignature)}`,
error?: string, );
const json = (await response.json()) as {
error?: string;
session?: { session?: {
key: string, key: string;
} };
}; };
if (json.error) { if (json.error) {
config.token = await createToken(config); config.token = await createToken(config);
@ -111,7 +133,12 @@ export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig:
return config; return config;
}; };
const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig, data: LastFmData, setConfig: SetConfType) => { const postSongDataToAPI = async (
songInfo: SongInfo,
config: LastFmPluginConfig,
data: LastFmData,
setConfig: SetConfType,
) => {
// This sends a post request to the api, and adds the common data // This sends a post request to the api, and adds the common data
if (!config.session_key) { if (!config.session_key) {
await getAndSetSessionKey(config, setConfig); await getAndSetSessionKey(config, setConfig);
@ -130,25 +157,35 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig,
postData.api_sig = createApiSig(postData, config.secret); postData.api_sig = createApiSig(postData, config.secret);
const formData = createFormData(postData); const formData = createFormData(postData);
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: formData }) net
.catch(async (error: { .fetch('https://ws.audioscrobbler.com/2.0/', {
response?: { method: 'POST',
data?: { body: formData,
error: number, })
.catch(
async (error: {
response?: {
data?: {
error: number;
};
};
}) => {
if (error?.response?.data?.error === 9) {
// Session key is invalid, so remove it from the config and reauthenticate
config.session_key = undefined;
config.token = await createToken(config);
await authenticate(config);
setConfig(config);
} }
} },
}) => { );
if (error?.response?.data?.error === 9) {
// Session key is invalid, so remove it from the config and reauthenticate
config.session_key = undefined;
config.token = await createToken(config);
await authenticate(config);
setConfig(config);
}
});
}; };
export const addScrobble = (songInfo: SongInfo, config: LastFmPluginConfig, setConfig: SetConfType) => { export const addScrobble = (
songInfo: SongInfo,
config: LastFmPluginConfig,
setConfig: SetConfType,
) => {
// This adds one scrobbled song to last.fm // This adds one scrobbled song to last.fm
const data = { const data = {
method: 'track.scrobble', method: 'track.scrobble',
@ -157,7 +194,11 @@ export const addScrobble = (songInfo: SongInfo, config: LastFmPluginConfig, setC
postSongDataToAPI(songInfo, config, data, setConfig); postSongDataToAPI(songInfo, config, data, setConfig);
}; };
export const setNowPlaying = (songInfo: SongInfo, config: LastFmPluginConfig, setConfig: SetConfType) => { export const setNowPlaying = (
songInfo: SongInfo,
config: LastFmPluginConfig,
setConfig: SetConfType,
) => {
// This sets the now playing status in last.fm // This sets the now playing status in last.fm
const data = { const data = {
method: 'track.updateNowPlaying', method: 'track.updateNowPlaying',

View File

@ -9,18 +9,18 @@ type LumiaData = {
url?: string; url?: string;
videoId?: string; videoId?: string;
playlistId?: string; playlistId?: string;
cover?: string|null; cover?: string | null;
cover_url?: string|null; cover_url?: string | null;
title?: string; title?: string;
artists?: string[]; artists?: string[];
status?: string; status?: string;
progress?: number; progress?: number;
duration?: number; duration?: number;
album_url?: string|null; album_url?: string | null;
album?: string|null; album?: string | null;
views?: number; views?: number;
isPaused?: boolean; isPaused?: boolean;
} };
export default createPlugin({ export default createPlugin({
name: 'Lumia Stream [beta]', name: 'Lumia Stream [beta]',
@ -30,7 +30,8 @@ export default createPlugin({
enabled: false, enabled: false,
}, },
backend() { backend() {
const secToMilisec = (t?: number) => t ? Math.round(Number(t) * 1e3) : undefined; const secToMilisec = (t?: number) =>
t ? Math.round(Number(t) * 1e3) : undefined;
const previousStatePaused = null; const previousStatePaused = null;
const data: LumiaData = { const data: LumiaData = {
@ -48,12 +49,17 @@ export default createPlugin({
} as const; } as const;
const url = `http://127.0.0.1:${port}/api/media`; const url = `http://127.0.0.1:${port}/api/media`;
net.fetch(url, { method: 'POST', body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }), headers }) net
.catch((error: { code: number, errno: number }) => { .fetch(url, {
method: 'POST',
body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }),
headers,
})
.catch((error: { code: number; errno: number }) => {
console.log( console.log(
`Error: '${ `Error: '${
error.code || error.errno error.code || error.errno
}' - when trying to access lumiastream webserver at port ${port}` }' - when trying to access lumiastream webserver at port ${port}`,
); );
}); });
}; };
@ -85,5 +91,5 @@ export default createPlugin({
data.views = songInfo.views; data.views = songInfo.views;
post(data); post(data);
}); });
} },
}); });

View File

@ -6,7 +6,7 @@ import { onRendererLoad } from './renderer';
export type LyricsGeniusPluginConfig = { export type LyricsGeniusPluginConfig = {
enabled: boolean; enabled: boolean;
romanizedLyrics: boolean; romanizedLyrics: boolean;
} };
export default createPlugin({ export default createPlugin({
name: 'Lyrics Genius', name: 'Lyrics Genius',

View File

@ -9,10 +9,14 @@ import type { LyricsGeniusPluginConfig } from './index';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
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 const onMainLoad = async ({ ipc, getConfig }: BackendContext<LyricsGeniusPluginConfig>) => { export const onMainLoad = async ({
ipc,
getConfig,
}: BackendContext<LyricsGeniusPluginConfig>) => {
const config = await getConfig(); const config = await getConfig();
if (config.romanizedLyrics) { if (config.romanizedLyrics) {
@ -38,7 +42,10 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
Genius Lyrics behavior is observed. Genius Lyrics behavior is observed.
*/ */
let hasAsianChars = false; let hasAsianChars = false;
if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) { if (
revRomanized &&
(eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))
) {
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`); lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
hasAsianChars = true; hasAsianChars = true;
} else { } else {
@ -62,7 +69,9 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
*/ */
const getLyricsList = async (queryString: string): Promise<string | null> => { const getLyricsList = async (queryString: string): Promise<string | null> => {
const response = await net.fetch( const response = await net.fetch(
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`, `https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(
queryString,
)}`,
); );
if (!response.ok) { if (!response.ok) {
return null; return null;
@ -71,11 +80,10 @@ const getLyricsList = async (queryString: string): Promise<string | null> => {
/* Fetch the first URL with the api, giving a collection of song results. /* Fetch the first URL with the api, giving a collection of song results.
Pick the first song, parsing the json given by the API. Pick the first song, parsing the json given by the API.
*/ */
const info = await response.json() as GetGeniusLyric; const info = (await response.json()) as GetGeniusLyric;
const url = info const url = info?.response?.sections?.find(
?.response (section) => section.type === 'song',
?.sections )?.hits[0]?.result?.url;
?.find((section) => section.type === 'song')?.hits[0]?.result?.url;
if (url) { if (url) {
return await getLyrics(url); return await getLyrics(url);

View File

@ -2,11 +2,16 @@ import type { SongInfo } from '@/providers/song-info';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index'; import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
export const onRendererLoad = ({ ipc: { invoke, on } }: RendererContext<LyricsGeniusPluginConfig>) => { export const onRendererLoad = ({
ipc: { invoke, on },
}: RendererContext<LyricsGeniusPluginConfig>) => {
const setLyrics = (lyricsContainer: Element, lyrics: string | null) => { const setLyrics = (lyricsContainer: Element, lyrics: string | null) => {
lyricsContainer.innerHTML = ` lyricsContainer.innerHTML = `
<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics"> <div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
${lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ?? 'Could not retrieve lyrics from genius'} ${
lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ??
'Could not retrieve lyrics from genius'
}
</div> </div>
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"> <yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline">
</yt-formatted-string> </yt-formatted-string>
@ -37,10 +42,10 @@ export const onRendererLoad = ({ ipc: { invoke, on } }: RendererContext<LyricsGe
// Check if disabled // Check if disabled
if (!tabs.lyrics?.hasAttribute('disabled')) return; if (!tabs.lyrics?.hasAttribute('disabled')) return;
const lyrics = await invoke( const lyrics = (await invoke(
'search-genius-lyrics', 'search-genius-lyrics',
extractedSongInfo, extractedSongInfo,
) as string | null; )) as string | null;
if (!lyrics) { if (!lyrics) {
// Delete previous lyrics if tab is open and couldn't get new lyrics // Delete previous lyrics if tab is open and couldn't get new lyrics

View File

@ -7,7 +7,8 @@ import backHTML from './templates/back.html?raw';
export default createPlugin({ export default createPlugin({
name: 'Navigation', name: 'Navigation',
description: 'Next/Back navigation arrows directly integrated in the interface, like in your favorite browser', description:
'Next/Back navigation arrows directly integrated in the interface, like in your favorite browser',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: true, enabled: true,

View File

@ -1,5 +1,9 @@
.navigation-item { .navigation-item {
font-family: Roboto, Noto Naskh Arabic UI, Arial, sans-serif; font-family:
Roboto,
Noto Naskh Arabic UI,
Arial,
sans-serif;
font-size: 20px; font-size: 20px;
line-height: var(--ytmusic-title-1_-_line-height); line-height: var(--ytmusic-title-1_-_line-height);
font-weight: 500; font-weight: 500;

View File

@ -1,33 +1,33 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
onclick="history.back()" onclick="history.back()"
role="tab" role="tab"
tab-id="FEmusic_back" tab-id="FEmusic_back"
> >
<div
aria-disabled="false"
class="search-icon style-scope ytmusic-search-box"
role="button"
tabindex="0"
title="Go to previous page"
>
<div <div
aria-disabled="false" class="tab-icon style-scope paper-icon-button navigation-icon"
class="search-icon style-scope ytmusic-search-box" id="icon"
role="button"
tabindex="0"
title="Go to previous page"
> >
<div <svg
class="tab-icon style-scope paper-icon-button navigation-icon" class="style-scope iron-icon"
id="icon" focusable="false"
> preserveAspectRatio="xMidYMid meet"
<svg style="pointer-events: none; display: block; width: 100%; height: 100%"
class="style-scope iron-icon" viewBox="0 0 492 492"
focusable="false" >
preserveAspectRatio="xMidYMid meet" <g class="style-scope iron-icon">
style="pointer-events: none; display: block; width: 100%; height: 100%" <path
viewBox="0 0 492 492" d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
> ></path>
<g class="style-scope iron-icon"> </g>
<path </svg>
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
></path>
</g>
</svg>
</div>
</div> </div>
</div>
</div> </div>

View File

@ -19,7 +19,7 @@
class="style-scope iron-icon" class="style-scope iron-icon"
focusable="false" focusable="false"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%;" style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 492 492" viewBox="0 0 492 492"
> >
<g class="style-scope iron-icon"> <g class="style-scope iron-icon">

View File

@ -23,8 +23,8 @@ export default createPlugin({
} }
// Remove the library button // Remove the library button
const libraryIconPath const libraryIconPath =
= 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z'; 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
const menuEntries = document.querySelectorAll( const menuEntries = document.querySelectorAll(
'#items ytmusic-guide-entry-renderer', '#items ytmusic-guide-entry-renderer',
@ -43,5 +43,5 @@ export default createPlugin({
childList: true, childList: true,
subtree: true, subtree: true,
}); });
} },
}); });

View File

@ -1,6 +1,6 @@
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"], .ytmusic-pivot-bar-renderer[tab-id='FEmusic_liked'],
ytmusic-guide-signin-promo-renderer, ytmusic-guide-signin-promo-renderer,
a[href="/music_premium"], a[href='/music_premium'],
.sign-in-link { .sign-in-link {
display: none !important; display: none !important;
} }

View File

@ -36,7 +36,8 @@ export const defaultConfig: NotificationsPluginConfig = {
export default createPlugin({ export default createPlugin({
name: 'Notifications', name: 'Notifications',
description: 'Display a notification when a song starts playing (interactive notifications are available on windows)', description:
'Display a notification when a song starts playing (interactive notifications are available on windows)',
restartNeeded: true, restartNeeded: true,
config: defaultConfig, config: defaultConfig,
menu: onMenu, menu: onMenu,

View File

@ -108,13 +108,19 @@ export default (
} }
return `\ return `\
content="${config().toastStyle ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\ content="${
config().toastStyle
? ''
: kind.charAt(0).toUpperCase() + kind.slice(1)
}"\
imageUri="file:///${selectIcon(kind)}" imageUri="file:///${selectIcon(kind)}"
`; `;
}; };
const getButton = (kind: keyof typeof mediaIcons) => const getButton = (kind: keyof typeof mediaIcons) =>
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`; `<action ${display(
kind,
)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
const getButtons = (isPaused: boolean) => `\ const getButtons = (isPaused: boolean) => `\
<actions> <actions>
@ -136,19 +142,32 @@ export default (
${getButtons(isPaused)} ${getButtons(isPaused)}
</toast>`; </toast>`;
const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\ const xmlImage = (
{ title, artist, isPaused }: SongInfo,
imgSrc: string,
placement: string,
) =>
toast(
`\
<image id="1" src="${imgSrc}" name="Image" ${placement}/> <image id="1" src="${imgSrc}" name="Image" ${placement}/>
<text id="1">${title}</text> <text id="1">${title}</text>
<text id="2">${artist}</text>\ <text id="2">${artist}</text>\
`, isPaused ?? false); `,
isPaused ?? false,
);
const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"'); const xmlLogo = (songInfo: SongInfo, imgSrc: string) =>
xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"'); const xmlHero = (songInfo: SongInfo, imgSrc: string) =>
xmlImage(songInfo, imgSrc, 'placement="hero"');
const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, ''); const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) =>
xmlImage(songInfo, imgSrc, '');
const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => toast(`\ const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) =>
toast(
`\
<image id="1" src="${imgSrc}" name="Image" /> <image id="1" src="${imgSrc}" name="Image" />
<text></text> <text></text>
<group> <group>
@ -158,37 +177,62 @@ export default (
</subgroup> </subgroup>
${xmlMoreData(songInfo)} ${xmlMoreData(songInfo)}
</group>\ </group>\
`, songInfo.isPaused ?? false); `,
songInfo.isPaused ?? false,
);
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\ const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
<subgroup hint-textStacking="bottom"> <subgroup hint-textStacking="bottom">
${album ${
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''} album
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text> ? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>`
: ''
}
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(
elapsedSeconds ?? 0,
)} / ${secondsToMinutes(songDuration)}</text>
</subgroup>\ </subgroup>\
`; `;
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\ const xmlBannerCenteredBottom = (
{ title, artist, isPaused }: SongInfo,
imgSrc: string,
) =>
toast(
`\
<text></text> <text></text>
<group> <group>
<subgroup hint-weight="1" hint-textStacking="center"> <subgroup hint-weight="1" hint-textStacking="center">
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text> <text hint-align="center" hint-style="${titleFontPicker(
title,
)}">${title}</text>
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text> <text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
</subgroup> </subgroup>
</group> </group>
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\ <image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
`, isPaused ?? false); `,
isPaused ?? false,
);
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\ const xmlBannerCenteredTop = (
{ title, artist, isPaused }: SongInfo,
imgSrc: string,
) =>
toast(
`\
<image id="1" src="${imgSrc}" name="Image" /> <image id="1" src="${imgSrc}" name="Image" />
<text></text> <text></text>
<group> <group>
<subgroup hint-weight="1" hint-textStacking="center"> <subgroup hint-weight="1" hint-textStacking="center">
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text> <text hint-align="center" hint-style="${titleFontPicker(
title,
)}">${title}</text>
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text> <text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
</subgroup> </subgroup>
</group>\ </group>\
`, isPaused ?? false); `,
isPaused ?? false,
);
const titleFontPicker = (title: string) => { const titleFontPicker = (title: string) => {
if (title.length <= 13) { if (title.length <= 13) {
@ -206,7 +250,6 @@ export default (
return 'Subtitle'; return 'Subtitle';
}; };
songControls = getSongControls(win); songControls = getSongControls(win);
let currentSeconds = 0; let currentSeconds = 0;
@ -226,8 +269,9 @@ export default (
} }
savedSongInfo = { ...songInfo }; savedSongInfo = { ...songInfo };
if (!songInfo.isPaused if (
&& (songInfo.url !== lastUrl || config().unpauseNotification) !songInfo.isPaused &&
(songInfo.url !== lastUrl || config().unpauseNotification)
) { ) {
lastUrl = songInfo.url; lastUrl = songInfo.url;
sendNotification(songInfo); sendNotification(songInfo);
@ -260,24 +304,21 @@ export default (
savedNotification?.close(); savedNotification?.close();
}); });
changeProtocolHandler( changeProtocolHandler((cmd) => {
(cmd) => { if (Object.keys(songControls).includes(cmd)) {
if (Object.keys(songControls).includes(cmd)) { songControls[cmd as keyof typeof songControls]();
songControls[cmd as keyof typeof songControls](); if (
if (config().refreshOnPlayPause && ( config().refreshOnPlayPause &&
cmd === 'pause' (cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
|| (cmd === 'play' && !config().unpauseNotification) ) {
) setImmediate(() =>
) { sendNotification({
setImmediate(() => ...savedSongInfo,
sendNotification({ isPaused: cmd === 'pause',
...savedSongInfo, elapsedSeconds: currentSeconds,
isPaused: cmd === 'pause', }),
elapsedSeconds: currentSeconds, );
}),
);
}
} }
}, }
); });
}; };

View File

@ -31,7 +31,10 @@ const setup = () => {
let currentUrl: string | undefined; let currentUrl: string | undefined;
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo) => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.unpauseNotification)) { if (
!songInfo.isPaused &&
(songInfo.url !== currentUrl || config.unpauseNotification)
) {
// Close the old notification // Close the old notification
oldNotification?.close(); oldNotification?.close();
currentUrl = songInfo.url; currentUrl = songInfo.url;
@ -43,11 +46,14 @@ const setup = () => {
}); });
}; };
export const onMainLoad = async (context: BackendContext<NotificationsPluginConfig>) => { export const onMainLoad = async (
context: BackendContext<NotificationsPluginConfig>,
) => {
config = await context.getConfig(); config = await context.getConfig();
// Register the callback for new song information // Register the callback for new song information
if (is.windows() && config.interactive) interactive(context.window, () => config, context); if (is.windows() && config.interactive)
interactive(context.window, () => config, context);
else setup(); else setup();
}; };

View File

@ -8,7 +8,10 @@ import type { NotificationsPluginConfig } from './index';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
export const onMenu = async ({ getConfig, setConfig }: MenuContext<NotificationsPluginConfig>): Promise<MenuTemplate> => { export const onMenu = async ({
getConfig,
setConfig,
}: MenuContext<NotificationsPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
const getToastStyleMenuItems = (options: NotificationsPluginConfig) => { const getToastStyleMenuItems = (options: NotificationsPluginConfig) => {
@ -38,7 +41,7 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<Notifications
checked: config.urgency === level.value, checked: config.urgency === level.value,
click: () => setConfig({ urgency: level.value }), click: () => setConfig({ urgency: level.value }),
})), })),
} },
]; ];
} else if (is.windows()) { } else if (is.windows()) {
return [ return [
@ -57,19 +60,22 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<Notifications
label: 'Open/Close on tray click', label: 'Open/Close on tray click',
type: 'checkbox', type: 'checkbox',
checked: config.trayControls, checked: config.trayControls,
click: (item: MenuItem) => setConfig({ trayControls: item.checked }), click: (item: MenuItem) =>
setConfig({ trayControls: item.checked }),
}, },
{ {
label: 'Hide Button Text', label: 'Hide Button Text',
type: 'checkbox', type: 'checkbox',
checked: config.hideButtonText, checked: config.hideButtonText,
click: (item: MenuItem) => setConfig({ hideButtonText: item.checked }), click: (item: MenuItem) =>
setConfig({ hideButtonText: item.checked }),
}, },
{ {
label: 'Refresh on Play/Pause', label: 'Refresh on Play/Pause',
type: 'checkbox', type: 'checkbox',
checked: config.refreshOnPlayPause, checked: config.refreshOnPlayPause,
click: (item: MenuItem) => setConfig({ refreshOnPlayPause: item.checked }), click: (item: MenuItem) =>
setConfig({ refreshOnPlayPause: item.checked }),
}, },
], ],
}, },

View File

@ -14,7 +14,6 @@ const userData = app.getPath('userData');
const temporaryIcon = path.join(userData, 'tempIcon.png'); const temporaryIcon = path.join(userData, 'tempIcon.png');
const temporaryBanner = path.join(userData, 'tempBanner.png'); const temporaryBanner = path.join(userData, 'tempBanner.png');
export const ToastStyles = { export const ToastStyles = {
logo: 1, logo: 1,
banner_centered_top: 2, banner_centered_top: 2,
@ -43,7 +42,10 @@ const nativeImageToLogo = cache((nativeImage: NativeImage) => {
}); });
}); });
export const notificationImage = (songInfo: SongInfo, config: NotificationsPluginConfig) => { export const notificationImage = (
songInfo: SongInfo,
config: NotificationsPluginConfig,
) => {
if (!songInfo.image) { if (!songInfo.image) {
return youtubeMusicIcon; return youtubeMusicIcon;
} }
@ -76,11 +78,10 @@ export const saveImage = cache((img: NativeImage, savePath: string) => {
return savePath; return savePath;
}); });
export const snakeToCamel = (string_: string) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) => export const snakeToCamel = (string_: string) =>
group.toUpperCase() string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
.replace('-', ' ') group.toUpperCase().replace('-', ' ').replace('_', ' '),
.replace('_', ' '), );
);
export const secondsToMinutes = (seconds: number) => { export const secondsToMinutes = (seconds: number) => {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);

View File

@ -6,16 +6,16 @@ import { onMenu } from './menu';
import { onPlayerApiReady, onRendererLoad } from './renderer'; import { onPlayerApiReady, onRendererLoad } from './renderer';
export type PictureInPicturePluginConfig = { export type PictureInPicturePluginConfig = {
'enabled': boolean; enabled: boolean;
'alwaysOnTop': boolean; alwaysOnTop: boolean;
'savePosition': boolean; savePosition: boolean;
'saveSize': boolean; saveSize: boolean;
'hotkey': 'P', hotkey: 'P';
'pip-position': [number, number]; 'pip-position': [number, number];
'pip-size': [number, number]; 'pip-size': [number, number];
'isInPiP': boolean; isInPiP: boolean;
'useNativePiP': boolean; useNativePiP: boolean;
} };
export default createPlugin({ export default createPlugin({
name: 'Picture In Picture', name: 'Picture In Picture',
@ -42,5 +42,5 @@ export default createPlugin({
renderer: { renderer: {
start: onRendererLoad, start: onRendererLoad,
onPlayerApiReady, onPlayerApiReady,
} },
}); });

View File

@ -6,14 +6,20 @@ import type { BackendContext } from '@/types/contexts';
let config: PictureInPicturePluginConfig; let config: PictureInPicturePluginConfig;
export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, handle, on } }: BackendContext<PictureInPicturePluginConfig>) => { export const onMainLoad = async ({
window,
getConfig,
setConfig,
ipc: { send, handle, on },
}: BackendContext<PictureInPicturePluginConfig>) => {
let isInPiP = false; let isInPiP = false;
let originalPosition: number[]; let originalPosition: number[];
let originalSize: number[]; let originalSize: number[];
let originalFullScreen: boolean; let originalFullScreen: boolean;
let originalMaximized: boolean; let originalMaximized: boolean;
const pipPosition = () => (config.savePosition && config['pip-position']) || [10, 10]; const pipPosition = () =>
(config.savePosition && config['pip-position']) || [10, 10];
const pipSize = () => (config.saveSize && config['pip-size']) || [450, 275]; const pipSize = () => (config.saveSize && config['pip-size']) || [450, 275];
const togglePiP = () => { const togglePiP = () => {
@ -50,7 +56,10 @@ export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, ha
window.setAlwaysOnTop(true, 'screen-saver', 1); window.setAlwaysOnTop(true, 'screen-saver', 1);
} }
} else { } else {
window.webContents.removeListener('before-input-event', blockShortcutsInPiP); window.webContents.removeListener(
'before-input-event',
blockShortcutsInPiP,
);
window.setMaximizable(true); window.setMaximizable(true);
window.setFullScreenable(true); window.setFullScreenable(true);
@ -76,7 +85,10 @@ export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, ha
window.setWindowButtonVisibility?.(!isInPiP); window.setWindowButtonVisibility?.(!isInPiP);
}; };
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => { const blockShortcutsInPiP = (
event: Electron.Event,
input: Electron.Input,
) => {
const key = input.key.toLowerCase(); const key = input.key.toLowerCase();
if (key === 'f') { if (key === 'f') {

View File

@ -7,8 +7,11 @@ import type { PictureInPicturePluginConfig } from './index';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
export const onMenu = async ({
export const onMenu = async ({ window, getConfig, setConfig }: MenuContext<PictureInPicturePluginConfig>): Promise<MenuTemplate> => { window,
getConfig,
setConfig,
}: MenuContext<PictureInPicturePluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
return [ return [
@ -42,17 +45,22 @@ export const onMenu = async ({ window, getConfig, setConfig }: MenuContext<Pictu
type: 'checkbox', type: 'checkbox',
checked: !!config.hotkey, checked: !!config.hotkey,
async click(item) { async click(item) {
const output = await prompt({ const output = await prompt(
title: 'Picture in Picture Hotkey', {
label: 'Choose a hotkey for toggling Picture in Picture', title: 'Picture in Picture Hotkey',
type: 'keybind', label: 'Choose a hotkey for toggling Picture in Picture',
keybindOptions: [{ type: 'keybind',
value: 'hotkey', keybindOptions: [
label: 'Hotkey', {
default: config.hotkey, value: 'hotkey',
}], label: 'Hotkey',
...promptOptions(), default: config.hotkey,
}, window); },
],
...promptOptions(),
},
window,
);
if (output) { if (output) {
const { value, accelerator } = output[0]; const { value, accelerator } = output[0];

View File

@ -51,14 +51,16 @@ const observer = new MutationObserver(() => {
if ( if (
menu.contains(pipButton) || menu.contains(pipButton) ||
!(menu.parentElement as (HTMLElement & { eventSink_: Element }) | null) !(
?.eventSink_ menu.parentElement as (HTMLElement & { eventSink_: Element }) | null
?.matches('ytmusic-menu-renderer.ytmusic-player-bar') )?.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')
) { ) {
return; return;
} }
const menuUrl = $<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; const menuUrl = $<HTMLAnchorElement>(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href;
if (!menuUrl?.includes('watch?')) { if (!menuUrl?.includes('watch?')) {
return; return;
} }
@ -79,8 +81,7 @@ const togglePictureInPicture = async () => {
await togglePiP(); await togglePiP();
$<HTMLButtonElement>('#icon')?.click(); // Close the menu $<HTMLButtonElement>('#icon')?.click(); // Close the menu
return true; return true;
} catch { } catch {}
}
} }
window.ipcRenderer.send('picture-in-picture'); window.ipcRenderer.send('picture-in-picture');
@ -94,10 +95,16 @@ const listenForToggle = () => {
const appLayout = $<HTMLElement>('ytmusic-app-layout'); const appLayout = $<HTMLElement>('ytmusic-app-layout');
const expandMenu = $<HTMLElement>('#expanding-menu'); const expandMenu = $<HTMLElement>('#expanding-menu');
const middleControls = $<HTMLButtonElement>('.middle-controls'); const middleControls = $<HTMLButtonElement>('.middle-controls');
const playerPage = $<HTMLElement & { playerPageOpen_: boolean }>('ytmusic-player-page'); const playerPage = $<HTMLElement & { playerPageOpen_: boolean }>(
const togglePlayerPageButton = $<HTMLButtonElement>('.toggle-player-page-button'); 'ytmusic-player-page',
);
const togglePlayerPageButton = $<HTMLButtonElement>(
'.toggle-player-page-button',
);
const fullScreenButton = $<HTMLButtonElement>('.fullscreen-button'); const fullScreenButton = $<HTMLButtonElement>('.fullscreen-button');
const player = $<HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }>('#player'); const player = $<
HTMLVideoElement & { onDoubleClick_: (() => void) | undefined }
>('#player');
const onPlayerDblClick = player?.onDoubleClick_; const onPlayerDblClick = player?.onDoubleClick_;
const mouseLeaveEventListener = () => middleControls?.click(); const mouseLeaveEventListener = () => middleControls?.click();
@ -106,9 +113,11 @@ const listenForToggle = () => {
window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => { window.ipcRenderer.on('pip-toggle', (_, isPip: boolean) => {
if (originalExitButton && player) { if (originalExitButton && player) {
if (isPip) { if (isPip) {
replaceButton('.exit-fullscreen-button', originalExitButton)?.addEventListener('click', () => togglePictureInPicture()); replaceButton(
player.onDoubleClick_ = () => { '.exit-fullscreen-button',
}; originalExitButton,
)?.addEventListener('click', () => togglePictureInPicture());
player.onDoubleClick_ = () => {};
expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener); expandMenu?.addEventListener('mouseleave', mouseLeaveEventListener);
if (!playerPage?.playerPageOpen_) { if (!playerPage?.playerPageOpen_) {
@ -134,7 +143,9 @@ const listenForToggle = () => {
}); });
}; };
export const onRendererLoad = async ({ getConfig }: RendererContext<PictureInPicturePluginConfig>) => { export const onRendererLoad = async ({
getConfig,
}: RendererContext<PictureInPicturePluginConfig>) => {
const config = await getConfig(); const config = await getConfig();
useNativePiP = config.useNativePiP; useNativePiP = config.useNativePiP;
@ -143,8 +154,8 @@ export const onRendererLoad = async ({ getConfig }: RendererContext<PictureInPic
const hotkeyEvent = toKeyEvent(config.hotkey); const hotkeyEvent = toKeyEvent(config.hotkey);
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if ( if (
keyEventAreEqual(event, hotkeyEvent) keyEventAreEqual(event, hotkeyEvent) &&
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) { ) {
togglePictureInPicture(); togglePictureInPicture();
} }
@ -155,17 +166,21 @@ export const onRendererLoad = async ({ getConfig }: RendererContext<PictureInPic
export const onPlayerApiReady = () => { export const onPlayerApiReady = () => {
listenForToggle(); listenForToggle();
cloneButton('.player-minimize-button')?.addEventListener('click', async () => { cloneButton('.player-minimize-button')?.addEventListener(
await togglePictureInPicture(); 'click',
setTimeout(() => $<HTMLButtonElement>('#player')?.click()); async () => {
}); await togglePictureInPicture();
setTimeout(() => $<HTMLButtonElement>('#player')?.click());
},
);
// Allows easily closing the menu by programmatically clicking outside of it // Allows easily closing the menu by programmatically clicking outside of it
$('#expanding-menu')?.removeAttribute('no-cancel-on-outside-click'); $('#expanding-menu')?.removeAttribute('no-cancel-on-outside-click');
// TODO: think about wether an additional button in songMenu is needed // TODO: think about wether an additional button in songMenu is needed
const popupContainer = $('ytmusic-popup-container'); const popupContainer = $('ytmusic-popup-container');
if (popupContainer) observer.observe(popupContainer, { if (popupContainer)
childList: true, observer.observe(popupContainer, {
subtree: true, childList: true,
}); subtree: true,
});
}; };

View File

@ -24,21 +24,21 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
y="0px" y="0px"
> >
<style type="text/css"> <style type="text/css">
.st0 { .st0 {
fill: #aaaaaa; fill: #aaaaaa;
} }
</style> </style>
<g id="XMLID_6_"> <g id="XMLID_6_">
<path <path
class="st0" class="st0"
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9 d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4 c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
v326.8H464.8z" v326.8H464.8z"
id="XMLID_11_" id="XMLID_11_"
/> />
</g> </g>
</svg> </svg>
</div> </div>
<div <div
class="text style-scope ytmusic-menu-navigation-item-renderer" class="text style-scope ytmusic-menu-navigation-item-renderer"

View File

@ -3,7 +3,8 @@ import { onPlayerApiReady, onUnload } from './renderer';
export default createPlugin({ export default createPlugin({
name: 'Playback Speed', name: 'Playback Speed',
description: 'Listen fast, listen slow! Adds a slider that controls song speed', description:
'Listen fast, listen slow! Adds a slider that controls song speed',
restartNeeded: false, restartNeeded: false,
config: { config: {
enabled: false, enabled: false,
@ -11,5 +12,5 @@ export default createPlugin({
renderer: { renderer: {
stop: onUnload, stop: onUnload,
onPlayerApiReady, onPlayerApiReady,
} },
}); });

View File

@ -29,7 +29,8 @@ const updatePlayBackSpeed = () => {
let menu: Element | null = null; let menu: Element | null = null;
const immediateValueChangedListener = (e: Event) => { const immediateValueChangedListener = (e: Event) => {
playbackSpeed = (e as CustomEvent<{ value: number; }>).detail.value || MIN_PLAYBACK_SPEED; playbackSpeed =
(e as CustomEvent<{ value: number }>).detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) { if (isNaN(playbackSpeed)) {
playbackSpeed = 1; playbackSpeed = 1;
} }
@ -38,7 +39,12 @@ const immediateValueChangedListener = (e: Event) => {
}; };
const setupSliderListener = singleton(() => { const setupSliderListener = singleton(() => {
document.querySelector('#playback-speed-slider')?.addEventListener('immediate-value-changed', immediateValueChangedListener); document
.querySelector('#playback-speed-slider')
?.addEventListener(
'immediate-value-changed',
immediateValueChangedListener,
);
}); });
const observePopupContainer = () => { const observePopupContainer = () => {
@ -49,9 +55,10 @@ const observePopupContainer = () => {
if ( if (
menu && menu &&
(menu.parentElement as HTMLElement & { eventSink_: Element | null }) (
?.eventSink_ menu.parentElement as HTMLElement & { eventSink_: Element | null }
?.matches('ytmusic-menu-renderer.ytmusic-player-bar')&& !menu.contains(slider) )?.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') &&
!menu.contains(slider)
) { ) {
menu.prepend(slider); menu.prepend(slider);
setupSliderListener(); setupSliderListener();
@ -82,14 +89,17 @@ const wheelEventListener = (e: WheelEvent) => {
} }
// E.deltaY < 0 means wheel-up // E.deltaY < 0 means wheel-up
playbackSpeed = roundToTwo(e.deltaY < 0 playbackSpeed = roundToTwo(
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED) e.deltaY < 0
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED), ? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
); );
updatePlayBackSpeed(); updatePlayBackSpeed();
// Update slider position // Update slider position
const playbackSpeedSilder = document.querySelector<HTMLElement & { value: number }>('#playback-speed-slider'); const playbackSpeedSilder = document.querySelector<
HTMLElement & { value: number }
>('#playback-speed-slider');
if (playbackSpeedSilder) { if (playbackSpeedSilder) {
playbackSpeedSilder.value = playbackSpeed; playbackSpeedSilder.value = playbackSpeed;
} }
@ -122,5 +132,10 @@ export const onUnload = () => {
} }
slider.removeEventListener('wheel', wheelEventListener); slider.removeEventListener('wheel', wheelEventListener);
getSongMenu()?.removeChild(slider); getSongMenu()?.removeChild(slider);
document.querySelector('#playback-speed-slider')?.removeEventListener('immediate-value-changed', immediateValueChangedListener); document
.querySelector('#playback-speed-slider')
?.removeEventListener(
'immediate-value-changed',
immediateValueChangedListener,
);
}; };

View File

@ -27,7 +27,7 @@
tabindex="0" tabindex="0"
title="Playback speed" title="Playback speed"
value="1" value="1"
><!--css-build:shady--> ><!--css-build:shady-->
<div class="style-scope tp-yt-paper-slider" id="sliderContainer"> <div class="style-scope tp-yt-paper-slider" id="sliderContainer">
<div class="bar-container style-scope tp-yt-paper-slider"> <div class="bar-container style-scope tp-yt-paper-slider">
<tp-yt-paper-progress <tp-yt-paper-progress
@ -41,7 +41,7 @@
role="progressbar" role="progressbar"
style="touch-action: none" style="touch-action: none"
value="1" value="1"
><!--css-build:shady--> ><!--css-build:shady-->
<div <div
class="style-scope tp-yt-paper-progress" class="style-scope tp-yt-paper-progress"
@ -61,10 +61,8 @@
</div> </div>
</tp-yt-paper-progress> </tp-yt-paper-progress>
</div> </div>
<dom-if class="style-scope tp-yt-paper-slider" <dom-if class="style-scope tp-yt-paper-slider">
> <template is="dom-if"></template>
<template is="dom-if"></template
>
</dom-if> </dom-if>
<div <div
class="slider-knob style-scope tp-yt-paper-slider" class="slider-knob style-scope tp-yt-paper-slider"
@ -77,11 +75,9 @@
></div> ></div>
</div> </div>
</div> </div>
<dom-if class="style-scope tp-yt-paper-slider" <dom-if class="style-scope tp-yt-paper-slider">
>
<template is="dom-if"></template> <template is="dom-if"></template>
</dom-if </dom-if>
>
</tp-yt-paper-slider> </tp-yt-paper-slider>
<div <div
class="text style-scope ytmusic-menu-navigation-item-renderer" class="text style-scope ytmusic-menu-navigation-item-renderer"

View File

@ -21,7 +21,8 @@ export type PreciseVolumePluginConfig = {
export default createPlugin({ export default createPlugin({
name: 'Precise Volume', name: 'Precise Volume',
description: 'Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps', description:
'Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -37,45 +38,76 @@ export default createPlugin({
menu: async ({ setConfig, getConfig, window }) => { menu: async ({ setConfig, getConfig, window }) => {
const config = await getConfig(); const config = await getConfig();
function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig) { function changeOptions(
changedOptions: Partial<PreciseVolumePluginConfig>,
options: PreciseVolumePluginConfig,
) {
for (const option in changedOptions) { for (const option in changedOptions) {
// HACK: Weird TypeScript error // HACK: Weird TypeScript error
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option]; (options as Record<string, unknown>)[option] = (
changedOptions as Record<string, unknown>
)[option];
} }
setConfig(options); setConfig(options);
} }
// Helper function for globalShortcuts prompt // Helper function for globalShortcuts prompt
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined }); const kb = (
label_: string,
value_: string,
default_: string,
): KeybindOptions => ({
value: value_,
label: label_,
default: default_ || undefined,
});
async function promptVolumeSteps(options: PreciseVolumePluginConfig) { async function promptVolumeSteps(options: PreciseVolumePluginConfig) {
const output = await prompt({ const output = await prompt(
title: 'Volume Steps', {
label: 'Choose Volume Increase/Decrease Steps', title: 'Volume Steps',
value: options.steps || 1, label: 'Choose Volume Increase/Decrease Steps',
type: 'counter', value: options.steps || 1,
counterOptions: { minimum: 0, maximum: 100, multiFire: true }, type: 'counter',
width: 380, counterOptions: { minimum: 0, maximum: 100, multiFire: true },
...promptOptions(), width: 380,
}, window); ...promptOptions(),
},
window,
);
if (output || output === 0) { // 0 is somewhat valid if (output || output === 0) {
// 0 is somewhat valid
changeOptions({ steps: output }, options); changeOptions({ steps: output }, options);
} }
} }
async function promptGlobalShortcuts(options: PreciseVolumePluginConfig, item: MenuItem) { async function promptGlobalShortcuts(
const output = await prompt({ options: PreciseVolumePluginConfig,
title: 'Global Volume Keybinds', item: MenuItem,
label: 'Choose Global Volume Keybinds:', ) {
type: 'keybind', const output = await prompt(
keybindOptions: [ {
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp), title: 'Global Volume Keybinds',
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown), label: 'Choose Global Volume Keybinds:',
], type: 'keybind',
...promptOptions(), keybindOptions: [
}, window); kb(
'Increase Volume',
'volumeUp',
options.globalShortcuts?.volumeUp,
),
kb(
'Decrease Volume',
'volumeDown',
options.globalShortcuts?.volumeDown,
),
],
...promptOptions(),
},
window,
);
if (output) { if (output) {
const newGlobalShortcuts: { const newGlobalShortcuts: {
@ -83,12 +115,15 @@ export default createPlugin({
volumeDown: string; volumeDown: string;
} = { volumeUp: '', volumeDown: '' }; } = { volumeUp: '', volumeDown: '' };
for (const { value, accelerator } of output) { for (const { value, accelerator } of output) {
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator; newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] =
accelerator;
} }
changeOptions({ globalShortcuts: newGlobalShortcuts }, options); changeOptions({ globalShortcuts: newGlobalShortcuts }, options);
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown); item.checked =
Boolean(options.globalShortcuts.volumeUp) ||
Boolean(options.globalShortcuts.volumeDown);
} else { } else {
// Reset checkbox if prompt was canceled // Reset checkbox if prompt was canceled
item.checked = !item.checked; item.checked = !item.checked;
@ -107,7 +142,10 @@ export default createPlugin({
{ {
label: 'Global Hotkeys', label: 'Global Hotkeys',
type: 'checkbox', type: 'checkbox',
checked: Boolean(config.globalShortcuts?.volumeUp ?? config.globalShortcuts?.volumeDown), checked: Boolean(
config.globalShortcuts?.volumeUp ??
config.globalShortcuts?.volumeDown,
),
click: (item) => promptGlobalShortcuts(config, item), click: (item) => promptGlobalShortcuts(config, item),
}, },
{ {
@ -121,11 +159,15 @@ export default createPlugin({
const config = await getConfig(); const config = await getConfig();
if (config.globalShortcuts?.volumeUp) { if (config.globalShortcuts?.volumeUp) {
globalShortcut.register(config.globalShortcuts.volumeUp, () => ipc.send('changeVolume', true)); globalShortcut.register(config.globalShortcuts.volumeUp, () =>
ipc.send('changeVolume', true),
);
} }
if (config.globalShortcuts?.volumeDown) { if (config.globalShortcuts?.volumeDown) {
globalShortcut.register(config.globalShortcuts.volumeDown, () => ipc.send('changeVolume', false)); globalShortcut.register(config.globalShortcuts.volumeDown, () =>
ipc.send('changeVolume', false),
);
} }
}, },
@ -135,5 +177,5 @@ export default createPlugin({
}, },
onPlayerApiReady, onPlayerApiReady,
onConfigChange, onConfigChange,
} },
}); });

View File

@ -13,11 +13,12 @@ function overrideAddEventListener() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
Element.prototype._addEventListener = Element.prototype.addEventListener; Element.prototype._addEventListener = Element.prototype.addEventListener;
// Override addEventListener to Ignore specific events in volume-slider // Override addEventListener to Ignore specific events in volume-slider
Element.prototype.addEventListener = function(type: string, listener: (event: Event) => void, useCapture = false) { Element.prototype.addEventListener = function (
if (!( type: string,
ignored.id.includes(this.id) listener: (event: Event) => void,
&& ignored.types.includes(type) useCapture = false,
)) { ) {
if (!(ignored.id.includes(this.id) && ignored.types.includes(type))) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
(this as any)._addEventListener(type, listener, useCapture); (this as any)._addEventListener(type, listener, useCapture);
} else if (window.electronIs.dev()) { } else if (window.electronIs.dev()) {
@ -29,11 +30,16 @@ function overrideAddEventListener() {
export const overrideListener = () => { export const overrideListener = () => {
overrideAddEventListener(); overrideAddEventListener();
// Restore original function after finished loading to avoid keeping Element.prototype altered // Restore original function after finished loading to avoid keeping Element.prototype altered
window.addEventListener('load', () => { window.addEventListener(
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access 'load',
Element.prototype.addEventListener = (Element.prototype as any)._addEventListener; () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access */
(Element.prototype as any)._addEventListener = undefined; Element.prototype.addEventListener = (
Element.prototype as any
}, { once: true }); )._addEventListener;
(Element.prototype as any)._addEventListener = undefined;
/* eslint-enable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access */
},
{ once: true },
);
}; };

View File

@ -24,7 +24,10 @@ export const moveVolumeHud = debounce((showVideo: boolean) => {
let options: PreciseVolumePluginConfig; let options: PreciseVolumePluginConfig;
export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: RendererContext<PreciseVolumePluginConfig>) => { export const onPlayerApiReady = async (
playerApi: YoutubePlayer,
context: RendererContext<PreciseVolumePluginConfig>,
) => {
options = await context.getConfig(); options = await context.getConfig();
api = playerApi; api = playerApi;
@ -57,14 +60,20 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
setupLocalArrowShortcuts(); setupLocalArrowShortcuts();
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue // Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none'; const noVid =
($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)
?.value === 'none';
injectVolumeHud(noVid); injectVolumeHud(noVid);
if (!noVid) { if (!noVid) {
setupVideoPlayerOnwheel(); setupVideoPlayerOnwheel();
if (!window.mainConfig.plugins.isEnabled('video-toggle')) { if (!window.mainConfig.plugins.isEnabled('video-toggle')) {
// Video-toggle handles hud positioning on its own // Video-toggle handles hud positioning on its own
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; const videoMode = () =>
$('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode())); api.getPlayerResponse().videoDetails?.musicVideoType !==
'MUSIC_VIDEO_TYPE_ATV';
$('video')?.addEventListener('srcChanged', () =>
moveVolumeHud(videoMode()),
);
} }
} }
} }
@ -80,7 +89,8 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
); );
} else { } else {
const position = 'top: 10px; left: 10px;'; const position = 'top: 10px; left: 10px;';
const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;'; const mainStyle =
'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
$('#song-video')?.insertAdjacentHTML( $('#song-video')?.insertAdjacentHTML(
'afterend', 'afterend',
@ -149,8 +159,11 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
// This checks that volume-slider was manually set // This checks that volume-slider was manually set
const target = mutation.target; const target = mutation.target;
const targetValueNumeric = Number(target.value); const targetValueNumeric = Number(target.value);
if (mutation.oldValue !== target.value if (
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) { mutation.oldValue !== target.value &&
(typeof options.savedVolume !== 'number' ||
Math.abs(options.savedVolume - targetValueNumeric) > 4)
) {
// Diff>4 means it was manually set // Diff>4 means it was manually set
setTooltip(targetValueNumeric); setTooltip(targetValueNumeric);
saveVolume(targetValueNumeric); saveVolume(targetValueNumeric);
@ -189,9 +202,11 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
function changeVolume(toIncrease: boolean) { function changeVolume(toIncrease: boolean) {
// Apply volume change if valid // Apply volume change if valid
const steps = Number(options.steps || 1); const steps = Number(options.steps || 1);
setVolume(toIncrease setVolume(
? Math.min(api.getVolume() + steps, 100) toIncrease
: Math.max(api.getVolume() - steps, 0)); ? Math.min(api.getVolume() + steps, 100)
: Math.max(api.getVolume() - steps, 0),
);
} }
function updateVolumeSlider() { function updateVolumeSlider() {
@ -200,7 +215,9 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
for (const slider of ['#volume-slider', '#expand-volume-slider']) { for (const slider of ['#volume-slider', '#expand-volume-slider']) {
const silderElement = $<HTMLInputElement>(slider); const silderElement = $<HTMLInputElement>(slider);
if (silderElement) { if (silderElement) {
silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume); silderElement.value = String(
savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume,
);
} }
} }
} }
@ -235,7 +252,9 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
function setupLocalArrowShortcuts() { function setupLocalArrowShortcuts() {
if (options.arrowsShortcut) { if (options.arrowsShortcut) {
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) { if (
$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) {
return; return;
} }
@ -256,7 +275,9 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
} }
} }
context.ipc.on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease)); context.ipc.on('changeVolume', (toIncrease: boolean) =>
changeVolume(toIncrease),
);
context.ipc.on('setVolume', (value: number) => setVolume(value)); context.ipc.on('setVolume', (value: number) => setVolume(value));
firstRun(); firstRun();

View File

@ -8,6 +8,6 @@
text-shadow: rgba(0, 0, 0, 0.5) 0px 0px 12px; text-shadow: rgba(0, 0, 0, 0.5) 0px 0px 12px;
} }
ytmusic-player[player-ui-state_="MINIPLAYER"] #volumeHud { ytmusic-player[player-ui-state_='MINIPLAYER'] #volumeHud {
top: 0 !important; top: 0 !important;
} }

View File

@ -9,28 +9,34 @@ import type { YoutubePlayer } from '@/types/youtube-player';
export default createPlugin({ export default createPlugin({
name: 'Video Quality Changer', name: 'Video Quality Changer',
description: 'Allows changing the video quality with a button on the video overlay', description:
'Allows changing the video quality with a button on the video overlay',
restartNeeded: false, restartNeeded: false,
config: { config: {
enabled: false, enabled: false,
}, },
backend({ ipc, window }) { backend({ ipc, window }) {
ipc.handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(window, { ipc.handle(
type: 'question', 'qualityChanger',
buttons: qualityLabels, async (qualityLabels: string[], currentIndex: number) =>
defaultId: currentIndex, await dialog.showMessageBox(window, {
title: 'Choose Video Quality', type: 'question',
message: 'Choose Video Quality:', buttons: qualityLabels,
detail: `Current Quality: ${qualityLabels[currentIndex]}`, defaultId: currentIndex,
cancelId: -1, title: 'Choose Video Quality',
})); message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1,
}),
);
}, },
renderer: { renderer: {
qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate), qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate),
onPlayerApiReady(api: YoutubePlayer, context) { onPlayerApiReady(api: YoutubePlayer, context) {
const getPlayer = () => document.querySelector<HTMLVideoElement>('#player'); const getPlayer = () =>
document.querySelector<HTMLVideoElement>('#player');
const chooseQuality = () => { const chooseQuality = () => {
setTimeout(() => getPlayer()?.click()); setTimeout(() => getPlayer()?.click());
@ -38,20 +44,27 @@ export default createPlugin({
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality()); const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
(context.ipc.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex) as Promise<{ response: number }>) (
.then((promise) => { context.ipc.invoke(
if (promise.response === -1) { 'qualityChanger',
return; api.getAvailableQualityLabels(),
} currentIndex,
) as Promise<{ response: number }>
).then((promise) => {
if (promise.response === -1) {
return;
}
const newQuality = qualityLevels[promise.response]; const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality); api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality); api.setPlaybackQuality(newQuality);
}); });
}; };
const setup = () => { const setup = () => {
document.querySelector('.top-row-buttons.ytmusic-player')?.prepend(this.qualitySettingsButton); document
.querySelector('.top-row-buttons.ytmusic-player')
?.prepend(this.qualitySettingsButton);
this.qualitySettingsButton.addEventListener('click', chooseQuality); this.qualitySettingsButton.addEventListener('click', chooseQuality);
}; };
@ -59,8 +72,9 @@ export default createPlugin({
setup(); setup();
}, },
stop() { stop() {
document.querySelector('.top-row-buttons.ytmusic-player')?.removeChild(this.qualitySettingsButton); document
.querySelector('.top-row-buttons.ytmusic-player')
?.removeChild(this.qualitySettingsButton);
}, },
} },
}); });

View File

@ -1,15 +1,25 @@
<tp-yt-paper-icon-button aria-disabled="false" aria-label="Open player quality changer" <tp-yt-paper-icon-button
class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings" role="button" aria-disabled="false"
tabindex="0" title="Open player quality changer"> aria-label="Open player quality changer"
class="player-quality-button style-scope ytmusic-player"
icon="yt-icons:settings"
role="button"
tabindex="0"
title="Open player quality changer"
>
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon"> <tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
<svg class="style-scope yt-icon" <svg
focusable="false" preserveAspectRatio="xMidYMid meet" class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;" focusable="false"
viewBox="0 0 24 24"> preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon"> <g class="style-scope yt-icon">
<path <path
class="style-scope yt-icon" class="style-scope yt-icon"
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path> d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
></path>
</g> </g>
</svg> </svg>
</tp-yt-iron-icon> </tp-yt-iron-icon>

View File

@ -12,11 +12,12 @@ export type ShortcutsPluginConfig = {
overrideMediaKeys: boolean; overrideMediaKeys: boolean;
global: ShortcutMappingType; global: ShortcutMappingType;
local: ShortcutMappingType; local: ShortcutMappingType;
} };
export default createPlugin({ export default createPlugin({
name: 'Shortcuts (& MPRIS)', name: 'Shortcuts (& MPRIS)',
description: 'Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users', description:
'Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,

View File

@ -9,20 +9,30 @@ import type { ShortcutMappingType, ShortcutsPluginConfig } from './index';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
function _registerGlobalShortcut(
function _registerGlobalShortcut(webContents: Electron.WebContents, shortcut: string, action: (webContents: Electron.WebContents) => void) { webContents: Electron.WebContents,
shortcut: string,
action: (webContents: Electron.WebContents) => void,
) {
globalShortcut.register(shortcut, () => { globalShortcut.register(shortcut, () => {
action(webContents); action(webContents);
}); });
} }
function _registerLocalShortcut(win: BrowserWindow, shortcut: string, action: (webContents: Electron.WebContents) => void) { function _registerLocalShortcut(
win: BrowserWindow,
shortcut: string,
action: (webContents: Electron.WebContents) => void,
) {
registerElectronLocalShortcut(win, shortcut, () => { registerElectronLocalShortcut(win, shortcut, () => {
action(win.webContents); action(win.webContents);
}); });
} }
export const onMainLoad = async ({ getConfig, window }: BackendContext<ShortcutsPluginConfig>) => { export const onMainLoad = async ({
getConfig,
window,
}: BackendContext<ShortcutsPluginConfig>) => {
const config = await getConfig(); const config = await getConfig();
const songControls = getSongControls(window); const songControls = getSongControls(window);
@ -45,7 +55,10 @@ export const onMainLoad = async ({ getConfig, window }: BackendContext<Shortcuts
const shortcutOptions = { global, local }; const shortcutOptions = { global, local };
for (const optionType in shortcutOptions) { for (const optionType in shortcutOptions) {
registerAllShortcuts(shortcutOptions[optionType as 'global' | 'local'], optionType); registerAllShortcuts(
shortcutOptions[optionType as 'global' | 'local'],
optionType,
);
} }
function registerAllShortcuts(container: ShortcutMappingType, type: string) { function registerAllShortcuts(container: ShortcutMappingType, type: string) {
@ -57,7 +70,12 @@ export const onMainLoad = async ({ getConfig, window }: BackendContext<Shortcuts
continue; // Action accelerator is empty continue; // Action accelerator is empty
} }
console.debug(`Registering ${type} shortcut`, container[action], ':', action); console.debug(
`Registering ${type} shortcut`,
container[action],
':',
action,
);
const actionCallback: () => void = songControls[action]; const actionCallback: () => void = songControls[action];
if (typeof actionCallback !== 'function') { if (typeof actionCallback !== 'function') {
console.warn('Invalid action', action); console.warn('Invalid action', action);
@ -65,8 +83,13 @@ export const onMainLoad = async ({ getConfig, window }: BackendContext<Shortcuts
} }
if (type === 'global') { if (type === 'global') {
_registerGlobalShortcut(window.webContents, container[action], actionCallback); _registerGlobalShortcut(
} else { // Type === "local" window.webContents,
container[action],
actionCallback,
);
} else {
// Type === "local"
_registerLocalShortcut(window, local[action], actionCallback); _registerLocalShortcut(window, local[action], actionCallback);
} }
} }

View File

@ -7,33 +7,49 @@ import type { BrowserWindow } from 'electron';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
export const onMenu = async ({ window, getConfig, setConfig }: MenuContext<ShortcutsPluginConfig>): Promise<MenuTemplate> => { export const onMenu = async ({
window,
getConfig,
setConfig,
}: MenuContext<ShortcutsPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig(); const config = await getConfig();
/** /**
* Helper function for keybind prompt * Helper function for keybind prompt
*/ */
const kb = (label_: string, value_: string, default_?: string): KeybindOptions => ({ value: value_, label: label_, default: default_ }); const kb = (
label_: string,
value_: string,
default_?: string,
): KeybindOptions => ({ value: value_, label: label_, default: default_ });
async function promptKeybind(config: ShortcutsPluginConfig, win: BrowserWindow) { async function promptKeybind(
const output = await prompt({ config: ShortcutsPluginConfig,
title: 'Global Keybinds', win: BrowserWindow,
label: 'Choose Global Keybinds for Songs Control:', ) {
type: 'keybind', const output = await prompt(
keybindOptions: [ // If default=undefined then no default is used {
kb('Previous', 'previous', config.global?.previous), title: 'Global Keybinds',
kb('Play / Pause', 'playPause', config.global?.playPause), label: 'Choose Global Keybinds for Songs Control:',
kb('Next', 'next', config.global?.next), type: 'keybind',
], keybindOptions: [
height: 270, // If default=undefined then no default is used
...promptOptions(), kb('Previous', 'previous', config.global?.previous),
}, win); kb('Play / Pause', 'playPause', config.global?.playPause),
kb('Next', 'next', config.global?.next),
],
height: 270,
...promptOptions(),
},
win,
);
if (output) { if (output) {
const newConfig = { ...config }; const newConfig = { ...config };
for (const { value, accelerator } of output) { for (const { value, accelerator } of output) {
newConfig.global[value as keyof ShortcutsPluginConfig['global']] = accelerator; newConfig.global[value as keyof ShortcutsPluginConfig['global']] =
accelerator;
} }
setConfig(config); setConfig(config);

View File

@ -3,7 +3,6 @@ declare module '@jellybrick/mpris-service' {
import { interface as dbusInterface } from 'dbus-next'; import { interface as dbusInterface } from 'dbus-next';
interface RootInterfaceOptions { interface RootInterfaceOptions {
identity: string; identity: string;
supportedUriSchemes: string[]; supportedUriSchemes: string[];
@ -105,14 +104,11 @@ declare module '@jellybrick/mpris-service' {
setProperty(property: string, valuePlain: unknown): void; setProperty(property: string, valuePlain: unknown): void;
} }
interface RootInterface { interface RootInterface {}
}
interface PlayerInterface { interface PlayerInterface {}
}
interface TracklistInterface { interface TracklistInterface {
TrackListReplaced(tracks: Track[]): void; TrackListReplaced(tracks: Track[]): void;
TrackAdded(afterTrack: string): void; TrackAdded(afterTrack: string): void;
@ -121,7 +117,6 @@ declare module '@jellybrick/mpris-service' {
} }
interface PlaylistsInterface { interface PlaylistsInterface {
PlaylistChanged(playlist: unknown[]): void; PlaylistChanged(playlist: unknown[]): void;
setActivePlaylistId(playlistId: string): void; setActivePlaylistId(playlistId: string): void;

View File

@ -21,14 +21,17 @@ function setupMPRIS() {
function registerMPRIS(win: BrowserWindow) { function registerMPRIS(win: BrowserWindow) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls; const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } =
songControls;
try { try {
// TODO: "Typing" for this arguments // TODO: "Typing" for this arguments
const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6); const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6);
const microToSec = (n: unknown) => Math.round(Number(n) / 1e6); const microToSec = (n: unknown) => Math.round(Number(n) / 1e6);
const seekTo = (e: { position: unknown }) => win.webContents.send('seekTo', microToSec(e.position)); const seekTo = (e: { position: unknown }) =>
const seekBy = (o: unknown) => win.webContents.send('seekBy', microToSec(o)); win.webContents.send('seekTo', microToSec(e.position));
const seekBy = (o: unknown) =>
win.webContents.send('seekBy', microToSec(o));
const player = setupMPRIS(); const player = setupMPRIS();
@ -42,7 +45,7 @@ function registerMPRIS(win: BrowserWindow) {
ipcMain.on('seeked', (_, t: number) => player.seeked(secToMicro(t))); ipcMain.on('seeked', (_, t: number) => player.seeked(secToMicro(t)));
let currentSeconds = 0; let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t); ipcMain.on('timeChanged', (_, t: number) => (currentSeconds = t));
ipcMain.on('repeatChanged', (_, mode: string) => { ipcMain.on('repeatChanged', (_, mode: string) => {
switch (mode) { switch (mode) {
@ -63,7 +66,11 @@ function registerMPRIS(win: BrowserWindow) {
}); });
player.on('loopStatus', (status: string) => { player.on('loopStatus', (status: string) => {
// SwitchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK]; const switches = [
mpris.LOOP_STATUS_NONE,
mpris.LOOP_STATUS_PLAYLIST,
mpris.LOOP_STATUS_TRACK,
];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
@ -91,7 +98,10 @@ function registerMPRIS(win: BrowserWindow) {
} }
}); });
player.on('playpause', () => { player.on('playpause', () => {
player.playbackStatus = player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING; player.playbackStatus =
player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING
? mpris.PLAYBACK_STATUS_PAUSED
: mpris.PLAYBACK_STATUS_PLAYING;
playPause(); playPause();
}); });
@ -106,7 +116,9 @@ function registerMPRIS(win: BrowserWindow) {
shuffle(); shuffle();
} }
}); });
player.on('open', (args: { uri: string }) => { win.loadURL(args.uri); }); player.on('open', (args: { uri: string }) => {
win.loadURL(args.uri);
});
let mprisVolNewer = false; let mprisVolNewer = false;
let autoUpdate = false; let autoUpdate = false;
@ -166,7 +178,9 @@ function registerMPRIS(win: BrowserWindow) {
player.metadata = data; player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds)); player.seeked(secToMicro(songInfo.elapsedSeconds));
player.playbackStatus = songInfo.isPaused ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING; player.playbackStatus = songInfo.isPaused
? mpris.PLAYBACK_STATUS_PAUSED
: mpris.PLAYBACK_STATUS_PLAYING;
} }
}); });
} catch (error) { } catch (error) {

View File

@ -17,5 +17,5 @@ export default createPlugin({
renderer: { renderer: {
start: onRendererLoad, start: onRendererLoad,
stop: onRendererUnload, stop: onRendererUnload,
} },
}); });

View File

@ -67,16 +67,15 @@ const audioCanPlayListener = (e: CustomEvent<Compressor>) => {
history += element; history += element;
} }
if (history == 0 // Silent if (
history == 0 && // Silent
&& !( !(
video && ( video &&
video.paused (video.paused ||
|| video.seeking video.seeking ||
|| video.ended video.ended ||
|| video.muted video.muted ||
|| video.volume === 0 video.volume === 0)
)
) )
) { ) {
isSilent = true; isSilent = true;
@ -112,23 +111,18 @@ const audioCanPlayListener = (e: CustomEvent<Compressor>) => {
video?.addEventListener('seeked', playOrSeekHandler); video?.addEventListener('seeked', playOrSeekHandler);
}; };
export const onRendererLoad = async ({ getConfig }: RendererContext<SkipSilencesPluginConfig>) => { export const onRendererLoad = async ({
getConfig,
}: RendererContext<SkipSilencesPluginConfig>) => {
config = await getConfig(); config = await getConfig();
document.addEventListener( document.addEventListener('audioCanPlay', audioCanPlayListener, {
'audioCanPlay', passive: true,
audioCanPlayListener, });
{
passive: true,
},
);
}; };
export const onRendererUnload = () => { export const onRendererUnload = () => {
document.removeEventListener( document.removeEventListener('audioCanPlay', audioCanPlayListener);
'audioCanPlay',
audioCanPlayListener,
);
if (playOrSeekHandler) { if (playOrSeekHandler) {
const video = document.querySelector('video'); const video = document.querySelector('video');

View File

@ -10,14 +10,22 @@ import type { Segment, SkipSegment } from './types';
export type SponsorBlockPluginConfig = { export type SponsorBlockPluginConfig = {
enabled: boolean; enabled: boolean;
apiURL: string; apiURL: string;
categories: ('sponsor' | 'intro' | 'outro' | 'interaction' | 'selfpromo' | 'music_offtopic')[]; categories: (
| 'sponsor'
| 'intro'
| 'outro'
| 'interaction'
| 'selfpromo'
| 'music_offtopic'
)[];
}; };
let currentSegments: Segment[] = []; let currentSegments: Segment[] = [];
export default createPlugin({ export default createPlugin({
name: 'SponsorBlock', name: 'SponsorBlock',
description: 'Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn\'t playing', description:
"Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing",
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -32,7 +40,11 @@ export default createPlugin({
], ],
} as SponsorBlockPluginConfig, } as SponsorBlockPluginConfig,
async backend({ getConfig, ipc }) { async backend({ getConfig, ipc }) {
const fetchSegments = async (apiURL: string, categories: string[], videoId: 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,
)}`; )}`;
@ -48,10 +60,8 @@ export default createPlugin({
return []; return [];
} }
const segments = await resp.json() as SkipSegment[]; const segments = (await resp.json()) as SkipSegment[];
return sortSegments( return sortSegments(segments.map((submission) => submission.segment));
segments.map((submission) => submission.segment),
);
} catch (error) { } catch (error) {
if (is.dev()) { if (is.dev()) {
console.log('error on sponsorblock request:', error); console.log('error on sponsorblock request:', error);
@ -66,7 +76,11 @@ export default createPlugin({
const { apiURL, categories } = config; const { apiURL, categories } = config;
ipc.on('video-src-changed', async (data: GetPlayerResponse) => { ipc.on('video-src-changed', async (data: GetPlayerResponse) => {
const segments = await fetchSegments(apiURL, categories, data?.videoDetails?.videoId); const segments = await fetchSegments(
apiURL,
categories,
data?.videoDetails?.videoId,
);
ipc.send('sponsorblock-skip', segments); ipc.send('sponsorblock-skip', segments);
}); });
}, },
@ -77,8 +91,8 @@ export default createPlugin({
for (const segment of currentSegments) { for (const segment of currentSegments) {
if ( if (
target.currentTime >= segment[0] target.currentTime >= segment[0] &&
&& target.currentTime < segment[1] target.currentTime < segment[1]
) { ) {
target.currentTime = segment[1]; target.currentTime = segment[1];
if (window.electronIs.dev()) { if (window.electronIs.dev()) {
@ -88,7 +102,7 @@ export default createPlugin({
} }
} }
}, },
resetSegments: () => currentSegments = [], resetSegments: () => (currentSegments = []),
start({ ipc }) { start({ ipc }) {
ipc.on('sponsorblock-skip', (segments: Segment[]) => { ipc.on('sponsorblock-skip', (segments: Segment[]) => {
currentSegments = segments; currentSegments = segments;
@ -108,6 +122,6 @@ export default createPlugin({
video.removeEventListener('timeupdate', this.timeUpdateListener); video.removeEventListener('timeupdate', this.timeUpdateListener);
video.removeEventListener('emptied', this.resetSegments); video.removeEventListener('emptied', this.resetSegments);
} },
} },
}); });

View File

@ -1,12 +1,13 @@
export type Segment = [number, number]; export type Segment = [number, number];
export interface SkipSegment { // Array of this object export interface SkipSegment {
// Array of this object
segment: Segment; //[0, 15.23] start and end time in seconds segment: Segment; //[0, 15.23] start and end time in seconds
UUID: string, UUID: string;
category: string, // [1] category: string; // [1]
videoDuration: number // Duration of video when submission occurred (to be used to determine when a submission is out of date). 0 when unknown. +- 1 second videoDuration: number; // Duration of video when submission occurred (to be used to determine when a submission is out of date). 0 when unknown. +- 1 second
actionType: string, // [3] actionType: string; // [3]
locked: number, // if submission is locked locked: number; // if submission is locked
votes: number, // Votes on segment votes: number; // Votes on segment
description: string, // title for chapters, empty string for other segments description: string; // title for chapters, empty string for other segments
} }

View File

@ -37,14 +37,18 @@ export default createPlugin({
click() { click() {
previous(); previous();
}, },
}, { },
{
tooltip: 'Play/Pause', tooltip: 'Play/Pause',
// Update icon based on play state // Update icon based on play state
icon: nativeImage.createFromPath(songInfo.isPaused ? get('play') : get('pause')), icon: nativeImage.createFromPath(
songInfo.isPaused ? get('play') : get('pause'),
),
click() { click() {
playPause(); playPause();
}, },
}, { },
{
tooltip: 'Next', tooltip: 'Next',
icon: nativeImage.createFromPath(get('next')), icon: nativeImage.createFromPath(get('next')),
click() { click() {
@ -81,5 +85,5 @@ export default createPlugin({
window.on('show', () => { window.on('show', () => {
setThumbar(currentSongInfo); setThumbar(currentSongInfo);
}); });
} },
}); });

View File

@ -70,8 +70,8 @@ export default createPlugin({
], ],
}); });
const { playPause, next, previous, dislike, like } =
const { playPause, next, previous, dislike, like } = getSongControls(window); getSongControls(window);
// If the page is ready, register the callback // If the page is ready, register the callback
window.once('ready-to-show', () => { window.once('ready-to-show', () => {
@ -95,5 +95,5 @@ export default createPlugin({
window.setTouchBar(touchBar); window.setTouchBar(touchBar);
}); });
}); });
} },
}); });

View File

@ -19,7 +19,7 @@ interface Data {
export default createPlugin({ export default createPlugin({
name: 'Tuna OBS', name: 'Tuna OBS',
description: 'Integration with OBS\'s plugin Tuna', description: "Integration with OBS's plugin Tuna",
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -48,18 +48,26 @@ export default createPlugin({
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
}; };
const url = `http://127.0.0.1:${port}/`; const url = `http://127.0.0.1:${port}/`;
net.fetch(url, { net
method: 'POST', .fetch(url, {
headers, method: 'POST',
body: JSON.stringify({ data }), headers,
}).catch((error: { code: number, errno: number }) => { body: JSON.stringify({ data }),
if (is.dev()) { })
console.debug(`Error: '${error.code || error.errno}' - when trying to access obs-tuna webserver at port ${port}`); .catch((error: { code: number; errno: number }) => {
} if (is.dev()) {
}); console.debug(
`Error: '${
error.code || error.errno
}' - when trying to access obs-tuna webserver at port ${port}`,
);
}
});
}; };
ipc.on('ytmd:player-api-loaded', () => ipc.send('setupTimeChangedListener')); ipc.on('ytmd:player-api-loaded', () =>
ipc.send('setupTimeChangedListener'),
);
ipc.on('timeChanged', (t: number) => { ipc.on('timeChanged', (t: number) => {
if (!this.data.title) { if (!this.data.title) {
return; return;
@ -85,6 +93,6 @@ export default createPlugin({
this.data.album = songInfo.album; this.data.album = songInfo.album;
post(this.data); post(this.data);
}); });
} },
} },
}); });

View File

@ -10,19 +10,23 @@ export interface Plugin<ConfigType extends Config> {
config: ConfigType; config: ConfigType;
} }
export interface RendererPlugin<ConfigType extends Config> extends Plugin<ConfigType> { export interface RendererPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
onEnable: (config: ConfigType) => void; onEnable: (config: ConfigType) => void;
} }
export interface MainPlugin<ConfigType extends Config> extends Plugin<ConfigType> { export interface MainPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
onEnable: (window: BrowserWindow, config: ConfigType) => string; onEnable: (window: BrowserWindow, config: ConfigType) => string;
} }
export interface PreloadPlugin<ConfigType extends Config> extends Plugin<ConfigType> { export interface PreloadPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
onEnable: (config: ConfigType) => void; onEnable: (config: ConfigType) => void;
} }
export interface MenuPlugin<ConfigType extends Config> extends Plugin<ConfigType> { export interface MenuPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
onEnable: (config: ConfigType) => void; onEnable: (config: ConfigType) => void;
} }

View File

@ -4,9 +4,18 @@ type Unregister = () => void;
let isLoaded = false; let isLoaded = false;
const cssToInject = new Map<string, ((unregister: Unregister) => void) | undefined>(); const cssToInject = new Map<
const cssToInjectFile = new Map<string, ((unregister: Unregister) => void) | undefined>(); string,
export const injectCSS = async (webContents: Electron.WebContents, css: string): Promise<Unregister> => { ((unregister: Unregister) => void) | undefined
>();
const cssToInjectFile = new Map<
string,
((unregister: Unregister) => void) | undefined
>();
export const injectCSS = async (
webContents: Electron.WebContents,
css: string,
): Promise<Unregister> => {
if (isLoaded) { if (isLoaded) {
const key = await webContents.insertCSS(css); const key = await webContents.insertCSS(css);
return async () => await webContents.removeInsertedCSS(key); return async () => await webContents.removeInsertedCSS(key);
@ -20,7 +29,10 @@ export const injectCSS = async (webContents: Electron.WebContents, css: string):
}); });
}; };
export const injectCSSAsFile = async (webContents: Electron.WebContents, filepath: string): Promise<Unregister> => { export const injectCSSAsFile = async (
webContents: Electron.WebContents,
filepath: string,
): Promise<Unregister> => {
if (isLoaded) { if (isLoaded) {
const key = await webContents.insertCSS(fs.readFileSync(filepath, 'utf-8')); const key = await webContents.insertCSS(fs.readFileSync(filepath, 'utf-8'));
return async () => await webContents.removeInsertedCSS(key); return async () => await webContents.removeInsertedCSS(key);
@ -47,7 +59,9 @@ const setupCssInjection = (webContents: Electron.WebContents) => {
}); });
cssToInjectFile.forEach(async (callback, filepath) => { cssToInjectFile.forEach(async (callback, filepath) => {
const key = await webContents.insertCSS(fs.readFileSync(filepath, 'utf-8')); const key = await webContents.insertCSS(
fs.readFileSync(filepath, 'utf-8'),
);
const remove = async () => await webContents.removeInsertedCSS(key); const remove = async () => await webContents.removeInsertedCSS(key);
callback?.(remove); callback?.(remove);

View File

@ -1,21 +1,22 @@
import { net } from 'electron'; import { net } from 'electron';
export const getNetFetchAsFetch = () => (async (input: RequestInfo | URL, init?: RequestInit) => { export const getNetFetchAsFetch = () =>
const url = (async (input: RequestInfo | URL, init?: RequestInit) => {
typeof input === 'string' const url =
? new URL(input) typeof input === 'string'
: input instanceof URL ? new URL(input)
: input instanceof URL
? input ? input
: new URL(input.url); : new URL(input.url);
if (init?.body && !init.method) { if (init?.body && !init.method) {
init.method = 'POST'; init.method = 'POST';
} }
const request = new Request( const request = new Request(
url, url,
input instanceof Request ? input : undefined, input instanceof Request ? input : undefined,
); );
return net.fetch(request, init); return net.fetch(request, init);
}) as typeof fetch; }) as typeof fetch;

View File

@ -2,7 +2,7 @@ import fs from 'node:fs';
export const fileExists = ( export const fileExists = (
path: fs.PathLike, path: fs.PathLike,
callbackIfExists: { (): void; (): void; (): void; }, callbackIfExists: { (): void; (): void; (): void },
callbackIfError: (() => void) | undefined = undefined, callbackIfError: (() => void) | undefined = undefined,
) => { ) => {
fs.access(path, fs.constants.F_OK, (error) => { fs.access(path, fs.constants.F_OK, (error) => {

View File

@ -1,7 +1,13 @@
import type { Config, MainPlugin, MenuPlugin, PreloadPlugin } from '../common'; import type { Config, MainPlugin, MenuPlugin, PreloadPlugin } from '../common';
export const defineMainPlugin = <ConfigType extends Config>(plugin: MainPlugin<ConfigType>) => plugin; export const defineMainPlugin = <ConfigType extends Config>(
plugin: MainPlugin<ConfigType>,
) => plugin;
export const definePreloadPlugin = <ConfigType extends Config>(plugin: PreloadPlugin<ConfigType>) => plugin; export const definePreloadPlugin = <ConfigType extends Config>(
plugin: PreloadPlugin<ConfigType>,
) => plugin;
export const defineMenuPlugin = <ConfigType extends Config>(plugin: MenuPlugin<ConfigType>) => plugin; export const defineMenuPlugin = <ConfigType extends Config>(
plugin: MenuPlugin<ConfigType>,
) => plugin;

View File

@ -25,7 +25,7 @@
} }
.video-toggle-custom-mode .video-switch-button:before { .video-toggle-custom-mode .video-switch-button:before {
content: "Video"; content: 'Video';
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -54,12 +54,16 @@
position: relative; position: relative;
} }
.video-toggle-custom-mode .video-switch-button-checkbox:checked + .video-switch-button-label:before { .video-toggle-custom-mode
.video-switch-button-checkbox:checked
+ .video-switch-button-label:before {
transform: translateX(10rem); transform: translateX(10rem);
transition: transform 300ms linear; transition: transform 300ms linear;
} }
.video-toggle-custom-mode .video-switch-button-checkbox + .video-switch-button-label { .video-toggle-custom-mode
.video-switch-button-checkbox
+ .video-switch-button-label {
position: relative; position: relative;
padding: 15px 0; padding: 15px 0;
display: block; display: block;
@ -67,8 +71,10 @@
pointer-events: none; pointer-events: none;
} }
.video-toggle-custom-mode .video-switch-button-checkbox + .video-switch-button-label:before { .video-toggle-custom-mode
content: ""; .video-switch-button-checkbox
+ .video-switch-button-label:before {
content: '';
background: rgba(60, 60, 60, 0.4); background: rgba(60, 60, 60, 0.4);
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

@ -13,11 +13,12 @@ export type VideoTogglePluginConfig = {
mode: 'custom' | 'native' | 'disabled'; mode: 'custom' | 'native' | 'disabled';
forceHide: boolean; forceHide: boolean;
align: 'left' | 'middle' | 'right'; align: 'left' | 'middle' | 'right';
} };
export default createPlugin({ export default createPlugin({
name: 'Video Toggle', name: 'Video Toggle',
description: 'Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab', description:
'Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab',
restartNeeded: true, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
@ -26,10 +27,7 @@ export default createPlugin({
forceHide: false, forceHide: false,
align: 'left', align: 'left',
} as VideoTogglePluginConfig, } as VideoTogglePluginConfig,
stylesheets: [ stylesheets: [buttonSwitcherStyle, forceHideStyle],
buttonSwitcherStyle,
forceHideStyle,
],
menu: async ({ getConfig, setConfig }) => { menu: async ({ getConfig, setConfig }) => {
const config = await getConfig(); const config = await getConfig();
@ -124,14 +122,22 @@ export default createPlugin({
switch (config.mode) { switch (config.mode) {
case 'native': { case 'native': {
document.querySelector('ytmusic-player-page')?.setAttribute('has-av-switcher', ''); document
document.querySelector('ytmusic-player')?.setAttribute('has-av-switcher', ''); .querySelector('ytmusic-player-page')
?.setAttribute('has-av-switcher', '');
document
.querySelector('ytmusic-player')
?.setAttribute('has-av-switcher', '');
return; return;
} }
case 'disabled': { case 'disabled': {
document.querySelector('ytmusic-player-page')?.removeAttribute('has-av-switcher'); document
document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher'); .querySelector('ytmusic-player-page')
?.removeAttribute('has-av-switcher');
document
.querySelector('ytmusic-player')
?.removeAttribute('has-av-switcher');
return; return;
} }
} }
@ -140,17 +146,22 @@ export default createPlugin({
const config = await getConfig(); const config = await getConfig();
this.config = config; this.config = config;
const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ? const moveVolumeHud = window.mainConfig.plugins.isEnabled(
preciseVolumeMoveVolumeHud as (_: boolean) => void 'precise-volume',
: (() => {}); )
? (preciseVolumeMoveVolumeHud as (_: boolean) => void)
: () => {};
const player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player'); const player = document.querySelector<
HTMLElement & { videoMode_: boolean }
>('ytmusic-player');
const video = document.querySelector<HTMLVideoElement>('video'); const video = document.querySelector<HTMLVideoElement>('video');
const switchButtonDiv = ElementFromHtml(buttonTemplate); const switchButtonDiv = ElementFromHtml(buttonTemplate);
const forceThumbnail = (img: HTMLImageElement) => { const forceThumbnail = (img: HTMLImageElement) => {
const thumbnails: ThumbnailElement[] = api?.getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? []; const thumbnails: ThumbnailElement[] =
api?.getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails ?? [];
if (thumbnails && thumbnails.length > 0) { if (thumbnails && thumbnails.length > 0) {
const thumbnail = thumbnails.at(-1)?.url.split('?')[0]; const thumbnail = thumbnails.at(-1)?.url.split('?')[0];
if (thumbnail) img.src = thumbnail; if (thumbnail) img.src = thumbnail;
@ -163,18 +174,28 @@ export default createPlugin({
} }
window.mainConfig.plugins.setOptions('video-toggle', config); window.mainConfig.plugins.setOptions('video-toggle', config);
const checkbox = document.querySelector<HTMLInputElement>('.video-switch-button-checkbox'); // custom mode const checkbox = document.querySelector<HTMLInputElement>(
'.video-switch-button-checkbox',
); // custom mode
if (checkbox) checkbox.checked = !config.hideVideo; if (checkbox) checkbox.checked = !config.hideVideo;
if (player) { if (player) {
player.style.margin = showVideo ? '' : 'auto 0px'; player.style.margin = showVideo ? '' : 'auto 0px';
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED'); player.setAttribute(
'playback-mode',
showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED',
);
document.querySelector<HTMLElement>('#song-video.ytmusic-player')!.style.display = showVideo ? 'block' : 'none'; document.querySelector<HTMLElement>(
document.querySelector<HTMLElement>('#song-image')!.style.display = showVideo ? 'none' : 'block'; '#song-video.ytmusic-player',
)!.style.display = showVideo ? 'block' : 'none';
document.querySelector<HTMLElement>('#song-image')!.style.display =
showVideo ? 'none' : 'block';
if (showVideo && video && !video.style.top) { if (showVideo && video && !video.style.top) {
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`; video.style.top = `${
(player.clientHeight - video.clientHeight) / 2
}px`;
} }
moveVolumeHud(showVideo); moveVolumeHud(showVideo);
@ -182,13 +203,17 @@ export default createPlugin({
}; };
const videoStarted = () => { const videoStarted = () => {
if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_ATV') { if (
api.getPlayerResponse().videoDetails.musicVideoType ===
'MUSIC_VIDEO_TYPE_ATV'
) {
// Video doesn't exist -> switch to song mode // Video doesn't exist -> switch to song mode
setVideoState(false); setVideoState(false);
// Hide toggle button // Hide toggle button
switchButtonDiv.style.display = 'none'; switchButtonDiv.style.display = 'none';
} else { } else {
const songImage = document.querySelector<HTMLImageElement>('#song-image img'); const songImage =
document.querySelector<HTMLImageElement>('#song-image img');
if (!songImage) { if (!songImage) {
return; return;
} }
@ -197,7 +222,11 @@ export default createPlugin({
// Show toggle button // Show toggle button
switchButtonDiv.style.display = 'initial'; switchButtonDiv.style.display = 'initial';
// Change display to video mode if video exist & video is hidden & option.hideVideo = false // Change display to video mode if video exist & video is hidden & option.hideVideo = false
if (!this.config?.hideVideo && document.querySelector<HTMLElement>('#song-video.ytmusic-player')?.style.display === 'none') { if (
!this.config?.hideVideo &&
document.querySelector<HTMLElement>('#song-video.ytmusic-player')
?.style.display === 'none'
) {
setVideoState(true); setVideoState(true);
} else { } else {
moveVolumeHud(!this.config?.hideVideo); moveVolumeHud(!this.config?.hideVideo);
@ -222,7 +251,9 @@ export default createPlugin({
} }
} }
}); });
playbackModeObserver.observe(player, { attributeFilter: ['playback-mode'] }); playbackModeObserver.observe(player, {
attributeFilter: ['playback-mode'],
});
} }
}; };
@ -243,11 +274,16 @@ export default createPlugin({
} }
} }
}); });
playbackModeObserver.observe(document.querySelector('#song-image img')!, { attributeFilter: ['src'] }); playbackModeObserver.observe(
document.querySelector('#song-image img')!,
{ attributeFilter: ['src'] },
);
}; };
if (config.mode !== 'native' && config.mode != 'disabled') { if (config.mode !== 'native' && config.mode != 'disabled') {
document.querySelector<HTMLVideoElement>('#player')?.prepend(switchButtonDiv); document
.querySelector<HTMLVideoElement>('#player')
?.prepend(switchButtonDiv);
setVideoState(!config.hideVideo); setVideoState(!config.hideVideo);
forcePlaybackMode(); forcePlaybackMode();

View File

@ -1,4 +1,4 @@
<div class="video-switch-button"> <div class="video-switch-button">
<input checked="true" class="video-switch-button-checkbox" type="checkbox"></input> <input checked="true" class="video-switch-button-checkbox" type="checkbox" />
<label class="video-switch-button-label" for=""><span class="video-switch-button-label-span">Song</span></label> <label class="video-switch-button-label" for=""><span class="video-switch-button-label-span">Song</span></label>
</div> </div>

View File

@ -10,24 +10,43 @@ declare module 'butterchurn' {
} }
class Visualizer { class Visualizer {
constructor(audioContext: AudioContext, canvas: HTMLCanvasElement, opts: ButterchurnOptions); constructor(
audioContext: AudioContext,
canvas: HTMLCanvasElement,
opts: ButterchurnOptions,
);
loseGLContext(): void; loseGLContext(): void;
connectAudio(audioNode: AudioNode): void; connectAudio(audioNode: AudioNode): void;
disconnectAudio(audioNode: AudioNode): void; disconnectAudio(audioNode: AudioNode): void;
static overrideDefaultVars(baseValsDefaults: unknown, baseVals: unknown): unknown; static overrideDefaultVars(
baseValsDefaults: unknown,
baseVals: unknown,
): unknown;
createQVars(): Record<string, WebAssembly.Global>; createQVars(): Record<string, WebAssembly.Global>;
createTVars(): Record<string, WebAssembly.Global>; createTVars(): Record<string, WebAssembly.Global>;
createPerFramePool(baseVals: unknown): Record<string, WebAssembly.Global>; createPerFramePool(baseVals: unknown): Record<string, WebAssembly.Global>;
createPerPixelPool(baseVals: unknown): Record<string, WebAssembly.Global>; createPerPixelPool(baseVals: unknown): Record<string, WebAssembly.Global>;
createCustomShapePerFramePool(baseVals: unknown): Record<string, WebAssembly.Global>; createCustomShapePerFramePool(
createCustomWavePerFramePool(baseVals: unknown): Record<string, WebAssembly.Global>; baseVals: unknown,
static makeShapeResetPool(pool: Record<string, WebAssembly.Global>, variables: string[], idx: number): Record<string, WebAssembly.Global>; ): Record<string, WebAssembly.Global>;
createCustomWavePerFramePool(
baseVals: unknown,
): Record<string, WebAssembly.Global>;
static makeShapeResetPool(
pool: Record<string, WebAssembly.Global>,
variables: string[],
idx: number,
): Record<string, WebAssembly.Global>;
static base64ToArrayBuffer(base64: string): ArrayBuffer; static base64ToArrayBuffer(base64: string): ArrayBuffer;
loadPreset(presetMap: unknown, blendTime?: number): Promise<void>; loadPreset(presetMap: unknown, blendTime?: number): Promise<void>;
async loadWASMPreset(preset: unknown, blendTime: number): Promise<void>; async loadWASMPreset(preset: unknown, blendTime: number): Promise<void>;
loadJSPreset(preset: unknown, blendTime: number): void; loadJSPreset(preset: unknown, blendTime: number): void;
loadExtraImages(imageData: unknown): void; loadExtraImages(imageData: unknown): void;
setRendererSize(width: number, height: number, opts?: VisualizerOptions): void; setRendererSize(
width: number,
height: number,
opts?: VisualizerOptions,
): void;
setInternalMeshSize(width: number, height: number): void; setInternalMeshSize(width: number, height: number): void;
setOutputAA(useAA: boolean): void; setOutputAA(useAA: boolean): void;
setCanvas(canvas: HTMLCanvasElement): void; setCanvas(canvas: HTMLCanvasElement): void;
@ -44,7 +63,11 @@ declare module 'butterchurn' {
} }
export default class Butterchurn { export default class Butterchurn {
static createVisualizer(audioContext: AudioContext, canvas: HTMLCanvasElement, options?: ButterchurnOptions): Visualizer; static createVisualizer(
audioContext: AudioContext,
canvas: HTMLCanvasElement,
options?: ButterchurnOptions,
): Visualizer;
} }
} }

View File

@ -4,7 +4,7 @@ import { Visualizer } from './visualizers/visualizer';
import { import {
ButterchurnVisualizer as butterchurn, ButterchurnVisualizer as butterchurn,
VudioVisualizer as vudio, VudioVisualizer as vudio,
WaveVisualizer as wave WaveVisualizer as wave,
} from './visualizers'; } from './visualizers';
type WaveColor = { type WaveColor = {
@ -19,7 +19,7 @@ export type VisualizerPluginConfig = {
preset: string; preset: string;
renderingFrequencyInMs: number; renderingFrequencyInMs: number;
blendTimeInSeconds: number; blendTimeInSeconds: number;
}, };
vudio: { vudio: {
effect: string; effect: string;
accuracy: number; accuracy: number;
@ -35,7 +35,7 @@ export type VisualizerPluginConfig = {
horizontalAlign: string; horizontalAlign: string;
verticalAlign: string; verticalAlign: string;
dottify: boolean; dottify: boolean;
} };
}; };
wave: { wave: {
animations: { animations: {
@ -51,7 +51,7 @@ export type VisualizerPluginConfig = {
lineColor?: string | WaveColor; lineColor?: string | WaveColor;
radius?: number; radius?: number;
frequencyBand?: string; frequencyBand?: string;
} };
}[]; }[];
}; };
}; };
@ -151,7 +151,7 @@ export default createPlugin({
const config = await getConfig(); const config = await getConfig();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let visualizerType: { new(...args: any[]): Visualizer<unknown> } = vudio; let visualizerType: { new (...args: any[]): Visualizer<unknown> } = vudio;
if (config.type === 'wave') { if (config.type === 'wave') {
visualizerType = wave; visualizerType = wave;
@ -162,12 +162,15 @@ export default createPlugin({
document.addEventListener( document.addEventListener(
'audioCanPlay', 'audioCanPlay',
(e) => { (e) => {
const video = document.querySelector<HTMLVideoElement & { captureStream(): MediaStream; }>('video'); const video = document.querySelector<
HTMLVideoElement & { captureStream(): MediaStream }
>('video');
if (!video) { if (!video) {
return; return;
} }
const visualizerContainer = document.querySelector<HTMLElement>('#player'); const visualizerContainer =
document.querySelector<HTMLElement>('#player');
if (!visualizerContainer) { if (!visualizerContainer) {
return; return;
} }
@ -210,7 +213,10 @@ export default createPlugin({
resizeVisualizer(canvas.width, canvas.height); resizeVisualizer(canvas.width, canvas.height);
const visualizerContainerObserver = new ResizeObserver((entries) => { const visualizerContainerObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
resizeVisualizer(entry.contentRect.width, entry.contentRect.height); resizeVisualizer(
entry.contentRect.width,
entry.contentRect.height,
);
} }
}); });
visualizerContainerObserver.observe(visualizerContainer); visualizerContainerObserver.observe(visualizerContainer);

View File

@ -30,14 +30,10 @@ class ButterchurnVisualizer extends Visualizer<Butterchurn> {
options, options,
); );
this.visualizer = Butterchurn.createVisualizer( this.visualizer = Butterchurn.createVisualizer(audioContext, canvas, {
audioContext, width: canvas.width,
canvas, height: canvas.height,
{ });
width: canvas.width,
height: canvas.height,
}
);
const preset = ButterchurnPresets[options.butterchurn.preset]; const preset = ButterchurnPresets[options.butterchurn.preset];
this.visualizer.loadPreset(preset, options.butterchurn.blendTimeInSeconds); this.visualizer.loadPreset(preset, options.butterchurn.blendTimeInSeconds);

View File

@ -45,8 +45,7 @@ class VudioVisualizer extends Visualizer<Vudio> {
}); });
} }
render() { render() {}
}
} }
export default VudioVisualizer; export default VudioVisualizer;

View File

@ -3,6 +3,7 @@ import { Wave } from '@foobar404/wave';
import { Visualizer } from './visualizer'; import { Visualizer } from './visualizer';
import type { VisualizerPluginConfig } from '../index'; import type { VisualizerPluginConfig } from '../index';
class WaveVisualizer extends Visualizer<Wave> { class WaveVisualizer extends Visualizer<Wave> {
name = 'wave'; name = 'wave';
@ -32,7 +33,10 @@ class WaveVisualizer extends Visualizer<Wave> {
canvas, canvas,
); );
for (const animation of options.wave.animations) { for (const animation of options.wave.animations) {
const TargetVisualizer = this.visualizer.animations[animation.type as keyof typeof this.visualizer.animations]; const TargetVisualizer =
this.visualizer.animations[
animation.type as keyof typeof this.visualizer.animations
];
this.visualizer.addAnimation( this.visualizer.addAnimation(
new TargetVisualizer(animation.config as never), // Magic of Typescript new TargetVisualizer(animation.config as never), // Magic of Typescript
@ -40,11 +44,9 @@ class WaveVisualizer extends Visualizer<Wave> {
} }
} }
resize(_: number, __: number) { resize(_: number, __: number) {}
}
render() { render() {}
}
} }
export default WaveVisualizer; export default WaveVisualizer;

View File

@ -9,7 +9,7 @@ declare module 'vudio/umd/vudio' {
fadeSide?: boolean; fadeSide?: boolean;
} }
interface WaveformOptions extends NoneWaveformOptions{ interface WaveformOptions extends NoneWaveformOptions {
horizontalAlign: 'left' | 'center' | 'right'; horizontalAlign: 'left' | 'center' | 'right';
verticalAlign: 'top' | 'middle' | 'bottom'; verticalAlign: 'top' | 'middle' | 'bottom';
} }
@ -19,11 +19,15 @@ declare module 'vudio/umd/vudio' {
accuracy?: number; accuracy?: number;
width?: number; width?: number;
height?: number; height?: number;
waveform?: WaveformOptions waveform?: WaveformOptions;
} }
class Vudio { class Vudio {
constructor(audio: HTMLAudioElement | MediaStream, canvas: HTMLCanvasElement, options: VudioOptions = {}); constructor(
audio: HTMLAudioElement | MediaStream,
canvas: HTMLCanvasElement,
options: VudioOptions = {},
);
dance(): void; dance(): void;
pause(): void; pause(): void;

View File

@ -9,7 +9,11 @@ export const restart = () => restartInternal();
export const setupAppControls = () => { export const setupAppControls = () => {
ipcMain.on('restart', restart); ipcMain.on('restart', restart);
ipcMain.handle('getDownloadsFolder', () => app.getPath('downloads')); ipcMain.handle('getDownloadsFolder', () => app.getPath('downloads'));
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow()?.webContents.loadURL(config.get('url'))); ipcMain.on(
'reload',
() =>
BrowserWindow.getFocusedWindow()?.webContents.loadURL(config.get('url')),
);
ipcMain.handle('getPath', (_, ...args: string[]) => path.join(...args)); ipcMain.handle('getPath', (_, ...args: string[]) => path.join(...args));
}; };
@ -25,9 +29,9 @@ function sendToFrontInternal(channel: string, ...args: unknown[]) {
} }
} }
export const sendToFront export const sendToFront =
= process.type === 'browser' process.type === 'browser'
? sendToFrontInternal ? sendToFrontInternal
: () => { : () => {
console.error('sendToFront called from renderer'); console.error('sendToFront called from renderer');
}; };

Some files were not shown because too many files have changed in this diff Show More