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

@ -30,19 +30,19 @@ export const loadAdBlockerEngine = async (
if (!fs.existsSync(cacheDirectory)) {
fs.mkdirSync(cacheDirectory);
}
const cachingOptions
= cache && additionalBlockLists.length === 0
? {
path: path.join(cacheDirectory, 'adblocker-engine.bin'),
read: promises.readFile,
write: promises.writeFile,
}
: undefined;
const cachingOptions =
cache && additionalBlockLists.length === 0
? {
path: path.join(cacheDirectory, 'adblocker-engine.bin'),
read: promises.readFile,
write: promises.writeFile,
}
: undefined;
const lists = [
...(
(disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) ? [] : SOURCES
),
...((disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0)
? []
: SOURCES),
...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 { createPlugin } from '@/utils';
import { isBlockerEnabled, loadAdBlockerEngine, unloadAdBlockerEngine } from './blocker';
import {
isBlockerEnabled,
loadAdBlockerEngine,
unloadAdBlockerEngine,
} from './blocker';
import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject';
@ -22,7 +26,7 @@ interface AdblockerConfig {
* Which adblocker to use.
* @default blockers.InPlayer
*/
blocker: typeof blockers[keyof typeof blockers];
blocker: (typeof blockers)[keyof typeof blockers];
/**
* Additional list of filters to use.
* @example ["https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"]
@ -86,7 +90,10 @@ export default createPlugin({
},
async onConfigChange(newConfig) {
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(
this.mainWindow.webContents.session,
newConfig.cache,
@ -117,5 +124,5 @@ export default createPlugin({
}
}
},
}
},
});

View File

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

View File

@ -8,7 +8,8 @@ import type { VideoDataChanged } from '@/types/video-data-changed';
export default createPlugin({
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,
config: {
enabled: false,
@ -62,13 +63,18 @@ export default createPlugin({
l = +(l * 100).toFixed(1);
//return "hsl(" + h + "," + s + "%," + l + "%)";
return [h,s,l];
return [h, s, l];
},
hue: 0,
saturation: 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) {
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
@ -84,19 +90,32 @@ export default createPlugin({
start() {
this.playerPage = document.querySelector<HTMLElement>('#player-page');
this.navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
this.ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
this.playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
this.navBarBackground = document.querySelector<HTMLElement>(
'#nav-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.sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
this.sidebarSmall = document.querySelector<HTMLElement>(
'#mini-guide-background',
);
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen = this.ytmusicAppLayout?.hasAttribute('player-page-open');
const isPageOpen =
this.ytmusicAppLayout?.hasAttribute('player-page-open');
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 {
if (this.sidebarSmall) {
this.sidebarSmall.style.backgroundColor = 'black';
@ -113,35 +132,84 @@ export default createPlugin({
onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor();
document.addEventListener('videodatachange', (event: CustomEvent<VideoDataChanged>) => {
if (event.detail.name === 'dataloaded') {
const playerResponse = playerApi.getPlayerResponse();
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (thumbnail) {
fastAverageColor.getColorAsync(thumbnail.url)
.then((albumColor) => {
if (albumColor) {
const [hue, saturation, lightness] = [this.hue, this.saturation, 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);
document.addEventListener(
'videodatachange',
(event: CustomEvent<VideoDataChanged>) => {
if (event.detail.name === 'dataloaded') {
const playerResponse = playerApi.getPlayerResponse();
const thumbnail =
playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (thumbnail) {
fastAverageColor
.getColorAsync(thumbnail.url)
.then((albumColor) => {
if (albumColor) {
const [hue, saturation, lightness] = ([
this.hue,
this.saturation,
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);
} else {
if (this.playerPage) {
this.playerPage.style.backgroundColor = '#000000';
}
}
})
.catch((e) => console.error(e));
})
.catch((e) => console.error(e));
}
}
}
});
},
);
},
}
},
});

View File

@ -4,23 +4,33 @@ yt-page-navigation-progress {
}
#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 {
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 {
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;
}
#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;
}

View File

@ -25,7 +25,8 @@ const defaultConfig: AmbientModePluginConfig = {
export default createPlugin({
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,
config: defaultConfig,
stylesheets: [style],
@ -133,7 +134,9 @@ export default createPlugin({
start() {
const injectBlurVideo = (): (() => void) | null => {
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');
if (!songVideo) return null;
@ -143,27 +146,34 @@ export default createPlugin({
const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas');
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
const context = blurCanvas.getContext('2d', {
willReadFrequently: true,
});
/* effect */
let lastEffectWorkId: number | null = null;
let lastImageData: ImageData | null = null;
const onSync = () => {
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
if (typeof lastEffectWorkId === 'number')
cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => {
// console.log('context', context);
if (!context) return;
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 (!height) return;
context.globalAlpha = 1;
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.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset;
@ -185,15 +195,17 @@ export default createPlugin({
if (newWidth === 0 || newHeight === 0) return;
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.height = `${newHeight * this.sizeRatio}px`;
if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen');
const leftOffset = newWidth * (this.sizeRatio - 1) / 2;
const topOffset = newHeight * (this.sizeRatio - 1) / 2;
const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
blurCanvas.style.setProperty('--blur', `${this.blur}px`);
@ -214,7 +226,10 @@ export default createPlugin({
/* hooking */
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();
observer.observe(songVideo, { attributes: true });
resizeObserver.observe(songVideo);
@ -226,7 +241,10 @@ export default createPlugin({
};
const onPlay = () => {
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('play', onPlay);
@ -249,7 +267,6 @@ export default createPlugin({
};
};
const playerPage = document.querySelector<HTMLElement>('#player-page');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
@ -262,7 +279,8 @@ export default createPlugin({
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
const isPageOpen =
ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
this.unregister?.();
this.unregister = injectBlurVideo() ?? null;
@ -293,6 +311,6 @@ export default createPlugin({
this.observer?.disconnect();
this.update = null;
this.unregister?.();
}
}
},
},
});

View File

@ -2,7 +2,8 @@ import { createPlugin } from '@/utils';
export default createPlugin({
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() {
document.addEventListener(

View File

@ -2,7 +2,7 @@ import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Bypass Age Restrictions',
description: 'bypass YouTube\'s age verification',
description: "bypass YouTube's age verification",
restartNeeded: true,
// 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"
class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles"
role="button" tabindex="0"
title="Open captions selector">
<tp-yt-paper-icon-button
aria-disabled="false"
aria-label="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">
<svg class="style-scope yt-icon"
focusable="false" preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%;"
viewBox="0 0 24 24">
<svg
class="style-scope yt-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon">
<path
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>
</svg>
</tp-yt-iron-icon>

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export type CrossfadePluginConfig = {
fadeOutDuration: number;
secondsBeforeEnd: number;
fadeScaling: 'linear' | 'logarithmic' | number;
}
};
export default createPlugin<
unknown,
@ -61,7 +61,10 @@ export default createPlugin<
fadeScaling: 'linear',
},
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(
{
title: 'Crossfade Options',
@ -89,8 +92,7 @@ export default createPlugin<
},
{
label: 'Crossfade x seconds before end',
value:
options.secondsBeforeEnd,
value: options.secondsBeforeEnd,
inputAttrs: {
type: 'number',
required: true,
@ -135,7 +137,10 @@ export default createPlugin<
{
label: 'Advanced',
async click() {
const newOptions = await promptCrossfadeValues(window, await getConfig());
const newOptions = await promptCrossfadeValues(
window,
await getConfig(),
);
if (newOptions) {
setConfig(newOptions);
}
@ -170,11 +175,14 @@ export default createPlugin<
let firstVideo = true;
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) => {
window.navigation.addEventListener('navigate', (event) => {
@ -184,9 +192,9 @@ export default createPlugin<
const nextVideoID = getVideoIDFromURL(event.destination.url ?? '');
if (
nextVideoID
&& currentVideoID
&& (firstVideo || nextVideoID !== currentVideoID)
nextVideoID &&
currentVideoID &&
(firstVideo || nextVideoID !== currentVideoID)
) {
if (isReadyToCrossfade()) {
crossfade(() => {
@ -245,8 +253,9 @@ export default createPlugin<
// Exit just before the end for the transition
const transitionBeforeEnd = () => {
if (
video.currentTime >= video.duration - this.config!.secondsBeforeEnd
&& isReadyToCrossfade()
video.currentTime >=
video.duration - this.config!.secondsBeforeEnd &&
isReadyToCrossfade()
) {
video.removeEventListener('timeupdate', transitionBeforeEnd);
@ -294,6 +303,6 @@ export default createPlugin<
createAudioForCrossfade(url);
});
}
}
},
},
});

View File

@ -6,7 +6,7 @@ import type { YoutubePlayer } from '@/types/youtube-player';
export type DisableAutoPlayPluginConfig = {
enabled: boolean;
applyOnce: boolean;
}
};
export default createPlugin<
unknown,
@ -53,7 +53,11 @@ export default createPlugin<
if (event.detail.name === 'dataloaded') {
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) {
@ -74,7 +78,6 @@ export default createPlugin<
},
onConfigChange(newConfig) {
this.config = newConfig;
}
}
},
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { app, BrowserWindow } from 'electron';
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 sendFeedback = (win: BrowserWindow, message?: unknown) => {

View File

@ -9,7 +9,10 @@ import type { MenuTemplate } from '@/menu';
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();
return [

View File

@ -28,7 +28,9 @@ const menuObserver = new MutationObserver(() => {
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) {
return;
}
@ -40,14 +42,18 @@ const menuObserver = new MutationObserver(() => {
return;
}
setTimeout(() => doneFirstLoad ||= true, 500);
setTimeout(() => (doneFirstLoad ||= true), 500);
});
export const onRendererLoad = ({ ipc }: RendererContext<DownloaderPluginConfig>) => {
export const onRendererLoad = ({
ipc,
}: RendererContext<DownloaderPluginConfig>) => {
window.download = () => {
let videoUrl = getSongMenu()
// 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');
if (videoUrl) {
if (videoUrl.startsWith('watch?')) {

View File

@ -16,7 +16,7 @@ export const DefaultPresetList: Record<string, Preset> = {
'Custom': {
extension: null,
ffmpegArgs: [],
}
},
};
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
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: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', 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: 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: '' },
{
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: 17,
container: '3gp',
content: 'audio/video',
resolution: '144p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 18,
container: 'mp4',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 22,
container: 'mp4',
content: 'audio/video',
resolution: '720p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 34,
container: 'flv',
content: 'audio/video',
resolution: '360p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 35,
container: 'flv',
content: 'audio/video',
resolution: '480p',
bitrate: '-',
range: '-',
vrOr3D: '-',
},
{
itag: 36,
container: '3gp',
content: 'audio/video',
resolution: '180p',
bitrate: '-',
range: '-',
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: 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({
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,
config: {
enabled: false,
@ -24,7 +25,8 @@ export default createPlugin({
);
Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
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);
// 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);
},
});
}
}
},
},
});

View File

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

View File

@ -5,7 +5,10 @@ import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
import type { BackendContext } from '@/types/contexts';
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', () => {
send('close-all-in-app-menu-panel');
});
@ -16,11 +19,13 @@ export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContex
});
});
handle(
'get-menu',
() => JSON.parse(JSON.stringify(
Menu.getApplicationMenu(),
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
handle('get-menu', () =>
JSON.parse(
JSON.stringify(
Menu.getApplicationMenu(),
(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();
let target: MenuItem | null = null;
const stack = [...menu?.items ?? []];
const stack = [...(menu?.items ?? [])];
while (stack.length > 0) {
const now = stack.shift();
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) => {
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) => {
const result = getMenuItemById(commandId);
return JSON.parse(JSON.stringify(
result,
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
return JSON.parse(
JSON.stringify(result, (key: string, value: unknown) =>
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 { 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();
if (is.linux()) {
@ -16,8 +19,8 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<InAppMenuConf
click(item) {
config.hideDOMWindowControls = item.checked;
setConfig(config);
}
}
},
},
];
}

View File

@ -1,9 +1,13 @@
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>',
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>',
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>',
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: {
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>',
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>',
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>',
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;
else iconWrapper.innerHTML = Icons.radio.unchecked;
} else {
const iconURL = typeof item.icon === 'string' ?
await window.ipcRenderer.invoke('image-path-to-data-url') as string : item.icon?.toDataURL();
const iconURL =
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})`;
}
@ -36,7 +40,8 @@ export const createPanel = (
const radioGroups: [MenuItem, HTMLElement][] = [];
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 iconWrapper = document.createElement('menu-icon');
@ -47,7 +52,10 @@ export const createPanel = (
menu.addEventListener('click', async () => {
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) {
updateIconState(iconWrapper, menuItem);
@ -56,10 +64,13 @@ export const createPanel = (
await Promise.all(
radioGroups.map(async ([item, iconWrapper]) => {
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);
})
}),
);
}
}
@ -74,10 +85,15 @@ export const createPanel = (
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
menu.appendChild(subMenuIcon);
const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], {
placement: 'right',
order: (options?.order ?? 0) + 1,
});
const [child, , children] = createPanel(
parent,
menu,
item.submenu?.items ?? [],
{
placement: 'right',
order: (options?.order ?? 0) + 1,
},
);
childPanels.push(child);
children.push(...children);
@ -106,7 +122,10 @@ export const createPanel = (
// long lists to squeeze their children at the bottom of the screen
// (This needs to be done *after* setAttribute)
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.classList.add('position-by-bottom');
}
@ -119,16 +138,17 @@ export const createPanel = (
document.body.addEventListener('click', (event) => {
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();
});
parent.appendChild(panel);
return [
panel,
{ isOpened, close, open },
childPanels,
] as const;
return [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';
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 hideDOMWindowControls = config.hideDOMWindowControls;
@ -70,7 +74,6 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
titleBar.appendChild(logo);
const addWindowControls = async () => {
// Create window control buttons
const minimizeButton = document.createElement('button');
minimizeButton.classList.add('window-control');
@ -124,12 +127,20 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
if (navBar) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(() => {
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
titleBar.style.setProperty(
'--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 () => {
@ -139,12 +150,16 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
});
panelClosers = [];
const menu = await invoke('get-menu') as Menu | null;
const menu = (await invoke('get-menu')) as Menu | null;
if (!menu) return;
menu.items.forEach((menuItem) => {
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);
menu.append(menuItem.label);
@ -153,7 +168,8 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
menu.style.visibility = 'hidden';
}
});
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
if (isNotWindowsOrMacOS && !hideDOMWindowControls)
await addWindowControls();
};
await updateMenu();
@ -164,13 +180,21 @@ export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: Rendere
});
on('refresh-in-app-menu', () => updateMenu());
on('window-maximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
on('window-unmaximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
if (
isNotWindowsOrMacOS &&
!hideDOMWindowControls &&
maximizeButton.firstChild
) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
@ -187,6 +211,9 @@ export const onPlayerApiReady = () => {
const htmlHeadStyle = document.querySelector('head > div > style');
if (htmlHeadStyle) {
// 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);
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 {
@ -64,18 +66,26 @@ menu-panel {
padding: 4px;
border-radius: 8px;
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);
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;
opacity: 0;
transform: scale(0.8);
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;
opacity: 1;
transform: scale(1);
@ -159,22 +169,32 @@ ytmusic-app-layout {
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;
}
#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] #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);
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 {
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) {
setNowPlaying(songInfo, config, setConfig);
// 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)) {
// 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);
}
}
});
}
},
});

View File

@ -6,21 +6,21 @@ import type { LastFmPluginConfig } from './index';
import type { SongInfo } from '@/providers/song-info';
interface LastFmData {
method: string,
timestamp?: number,
method: string;
timestamp?: number;
}
interface LastFmSongData {
track?: string,
duration?: number,
artist?: string,
album?: string,
api_key: string,
sk?: string,
format: string,
method: string,
timestamp?: number,
api_sig?: string,
track?: string;
duration?: number;
artist?: string;
album?: string;
api_key: string;
sk?: string;
format: string;
method: string;
timestamp?: number;
api_sig?: string;
}
const createFormData = (parameters: LastFmSongData) => {
@ -33,12 +33,19 @@ const createFormData = (parameters: LastFmSongData) => {
return formData;
};
const createQueryString = (parameters: Record<string, unknown>, apiSignature: string) => {
const createQueryString = (
parameters: Record<string, unknown>,
apiSignature: string,
) => {
// Creates a querystring
const queryData = [];
parameters.api_sig = apiSignature;
for (const key in parameters) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(parameters[key]))}`);
queryData.push(
`${encodeURIComponent(key)}=${encodeURIComponent(
String(parameters[key]),
)}`,
);
}
return '?' + queryData.join('&');
@ -63,7 +70,11 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
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
const data = {
method: 'auth.gettoken',
@ -71,19 +82,28 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
format: 'json',
};
const apiSigature = createApiSig(data, secret);
const response = await net.fetch(`${apiRoot}${createQueryString(data, apiSigature)}`);
const json = await response.json() as Record<string, string>;
const response = await net.fetch(
`${apiRoot}${createQueryString(data, apiSigature)}`,
);
const json = (await response.json()) as Record<string, string>;
return json?.token;
};
const authenticate = async (config: LastFmPluginConfig) => {
// 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
const data = {
api_key: config.api_key,
@ -92,12 +112,14 @@ export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig:
token: config.token,
};
const apiSignature = createApiSig(data, config.secret);
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
const json = await response.json() as {
error?: string,
const response = await net.fetch(
`${config.api_root}${createQueryString(data, apiSignature)}`,
);
const json = (await response.json()) as {
error?: string;
session?: {
key: string,
}
key: string;
};
};
if (json.error) {
config.token = await createToken(config);
@ -111,7 +133,12 @@ export const getAndSetSessionKey = async (config: LastFmPluginConfig, setConfig:
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
if (!config.session_key) {
await getAndSetSessionKey(config, setConfig);
@ -130,25 +157,35 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFmPluginConfig,
postData.api_sig = createApiSig(postData, config.secret);
const formData = createFormData(postData);
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: formData })
.catch(async (error: {
response?: {
data?: {
error: number,
net
.fetch('https://ws.audioscrobbler.com/2.0/', {
method: 'POST',
body: formData,
})
.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
const data = {
method: 'track.scrobble',
@ -157,7 +194,11 @@ export const addScrobble = (songInfo: SongInfo, config: LastFmPluginConfig, setC
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
const data = {
method: 'track.updateNowPlaying',

View File

@ -9,18 +9,18 @@ type LumiaData = {
url?: string;
videoId?: string;
playlistId?: string;
cover?: string|null;
cover_url?: string|null;
cover?: string | null;
cover_url?: string | null;
title?: string;
artists?: string[];
status?: string;
progress?: number;
duration?: number;
album_url?: string|null;
album?: string|null;
album_url?: string | null;
album?: string | null;
views?: number;
isPaused?: boolean;
}
};
export default createPlugin({
name: 'Lumia Stream [beta]',
@ -30,7 +30,8 @@ export default createPlugin({
enabled: false,
},
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 data: LumiaData = {
@ -48,12 +49,17 @@ export default createPlugin({
} as const;
const url = `http://127.0.0.1:${port}/api/media`;
net.fetch(url, { method: 'POST', body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }), headers })
.catch((error: { code: number, errno: number }) => {
net
.fetch(url, {
method: 'POST',
body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }),
headers,
})
.catch((error: { code: number; errno: number }) => {
console.log(
`Error: '${
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;
post(data);
});
}
},
});

View File

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

View File

@ -9,10 +9,14 @@ import type { LyricsGeniusPluginConfig } from './index';
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;
export const onMainLoad = async ({ ipc, getConfig }: BackendContext<LyricsGeniusPluginConfig>) => {
export const onMainLoad = async ({
ipc,
getConfig,
}: BackendContext<LyricsGeniusPluginConfig>) => {
const config = await getConfig();
if (config.romanizedLyrics) {
@ -38,7 +42,10 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
Genius Lyrics behavior is observed.
*/
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`);
hasAsianChars = true;
} else {
@ -62,7 +69,9 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
*/
const getLyricsList = async (queryString: string): Promise<string | null> => {
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) {
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.
Pick the first song, parsing the json given by the API.
*/
const info = await response.json() as GetGeniusLyric;
const url = info
?.response
?.sections
?.find((section) => section.type === 'song')?.hits[0]?.result?.url;
const info = (await response.json()) as GetGeniusLyric;
const url = info?.response?.sections?.find(
(section) => section.type === 'song',
)?.hits[0]?.result?.url;
if (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 { 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) => {
lyricsContainer.innerHTML = `
<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>
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline">
</yt-formatted-string>
@ -37,10 +42,10 @@ export const onRendererLoad = ({ ipc: { invoke, on } }: RendererContext<LyricsGe
// Check if disabled
if (!tabs.lyrics?.hasAttribute('disabled')) return;
const lyrics = await invoke(
const lyrics = (await invoke(
'search-genius-lyrics',
extractedSongInfo,
) as string | null;
)) as string | null;
if (!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({
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,
config: {
enabled: true,

View File

@ -1,5 +1,9 @@
.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;
line-height: var(--ytmusic-title-1_-_line-height);
font-weight: 500;

View File

@ -1,33 +1,33 @@
<div
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
onclick="history.back()"
role="tab"
tab-id="FEmusic_back"
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
onclick="history.back()"
role="tab"
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
aria-disabled="false"
class="search-icon style-scope ytmusic-search-box"
role="button"
tabindex="0"
title="Go to previous page"
class="tab-icon style-scope paper-icon-button navigation-icon"
id="icon"
>
<div
class="tab-icon style-scope paper-icon-button navigation-icon"
id="icon"
>
<svg
class="style-scope iron-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 492 492"
>
<g class="style-scope iron-icon">
<path
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>
<svg
class="style-scope iron-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 492 492"
>
<g class="style-scope iron-icon">
<path
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>

View File

@ -19,7 +19,7 @@
class="style-scope iron-icon"
focusable="false"
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"
>
<g class="style-scope iron-icon">

View File

@ -23,8 +23,8 @@ export default createPlugin({
}
// Remove the library button
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';
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';
const observer = new MutationObserver(() => {
const menuEntries = document.querySelectorAll(
'#items ytmusic-guide-entry-renderer',
@ -43,5 +43,5 @@ export default createPlugin({
childList: 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,
a[href="/music_premium"],
a[href='/music_premium'],
.sign-in-link {
display: none !important;
}

View File

@ -36,7 +36,8 @@ export const defaultConfig: NotificationsPluginConfig = {
export default createPlugin({
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,
config: defaultConfig,
menu: onMenu,

View File

@ -108,13 +108,19 @@ export default (
}
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)}"
`;
};
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) => `\
<actions>
@ -136,19 +142,32 @@ export default (
${getButtons(isPaused)}
</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}/>
<text id="1">${title}</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" />
<text></text>
<group>
@ -158,37 +177,62 @@ export default (
</subgroup>
${xmlMoreData(songInfo)}
</group>\
`, songInfo.isPaused ?? false);
`,
songInfo.isPaused ?? false,
);
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
<subgroup hint-textStacking="bottom">
${album
? `<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>
${
album
? `<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>\
`;
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
const xmlBannerCenteredBottom = (
{ title, artist, isPaused }: SongInfo,
imgSrc: string,
) =>
toast(
`\
<text></text>
<group>
<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>
</subgroup>
</group>
<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" />
<text></text>
<group>
<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>
</subgroup>
</group>\
`, isPaused ?? false);
`,
isPaused ?? false,
);
const titleFontPicker = (title: string) => {
if (title.length <= 13) {
@ -206,7 +250,6 @@ export default (
return 'Subtitle';
};
songControls = getSongControls(win);
let currentSeconds = 0;
@ -226,8 +269,9 @@ export default (
}
savedSongInfo = { ...songInfo };
if (!songInfo.isPaused
&& (songInfo.url !== lastUrl || config().unpauseNotification)
if (
!songInfo.isPaused &&
(songInfo.url !== lastUrl || config().unpauseNotification)
) {
lastUrl = songInfo.url;
sendNotification(songInfo);
@ -260,24 +304,21 @@ export default (
savedNotification?.close();
});
changeProtocolHandler(
(cmd) => {
if (Object.keys(songControls).includes(cmd)) {
songControls[cmd as keyof typeof songControls]();
if (config().refreshOnPlayPause && (
cmd === 'pause'
|| (cmd === 'play' && !config().unpauseNotification)
)
) {
setImmediate(() =>
sendNotification({
...savedSongInfo,
isPaused: cmd === 'pause',
elapsedSeconds: currentSeconds,
}),
);
}
changeProtocolHandler((cmd) => {
if (Object.keys(songControls).includes(cmd)) {
songControls[cmd as keyof typeof songControls]();
if (
config().refreshOnPlayPause &&
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
) {
setImmediate(() =>
sendNotification({
...savedSongInfo,
isPaused: cmd === 'pause',
elapsedSeconds: currentSeconds,
}),
);
}
},
);
}
});
};

View File

@ -31,7 +31,10 @@ const setup = () => {
let currentUrl: string | undefined;
registerCallback((songInfo: SongInfo) => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.unpauseNotification)) {
if (
!songInfo.isPaused &&
(songInfo.url !== currentUrl || config.unpauseNotification)
) {
// Close the old notification
oldNotification?.close();
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();
// 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();
};

View File

@ -8,7 +8,10 @@ import type { NotificationsPluginConfig } from './index';
import type { MenuTemplate } from '@/menu';
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 getToastStyleMenuItems = (options: NotificationsPluginConfig) => {
@ -38,7 +41,7 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<Notifications
checked: config.urgency === level.value,
click: () => setConfig({ urgency: level.value }),
})),
}
},
];
} else if (is.windows()) {
return [
@ -57,19 +60,22 @@ export const onMenu = async ({ getConfig, setConfig }: MenuContext<Notifications
label: 'Open/Close on tray click',
type: 'checkbox',
checked: config.trayControls,
click: (item: MenuItem) => setConfig({ trayControls: item.checked }),
click: (item: MenuItem) =>
setConfig({ trayControls: item.checked }),
},
{
label: 'Hide Button Text',
type: 'checkbox',
checked: config.hideButtonText,
click: (item: MenuItem) => setConfig({ hideButtonText: item.checked }),
click: (item: MenuItem) =>
setConfig({ hideButtonText: item.checked }),
},
{
label: 'Refresh on Play/Pause',
type: 'checkbox',
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 temporaryBanner = path.join(userData, 'tempBanner.png');
export const ToastStyles = {
logo: 1,
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) {
return youtubeMusicIcon;
}
@ -76,11 +78,10 @@ export const saveImage = cache((img: NativeImage, savePath: string) => {
return savePath;
});
export const snakeToCamel = (string_: string) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
group.toUpperCase()
.replace('-', ' ')
.replace('_', ' '),
);
export const snakeToCamel = (string_: string) =>
string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
group.toUpperCase().replace('-', ' ').replace('_', ' '),
);
export const secondsToMinutes = (seconds: number) => {
const minutes = Math.floor(seconds / 60);

View File

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

View File

@ -6,14 +6,20 @@ import type { BackendContext } from '@/types/contexts';
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 originalPosition: number[];
let originalSize: number[];
let originalFullScreen: 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 togglePiP = () => {
@ -50,7 +56,10 @@ export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, ha
window.setAlwaysOnTop(true, 'screen-saver', 1);
}
} else {
window.webContents.removeListener('before-input-event', blockShortcutsInPiP);
window.webContents.removeListener(
'before-input-event',
blockShortcutsInPiP,
);
window.setMaximizable(true);
window.setFullScreenable(true);
@ -76,7 +85,10 @@ export const onMainLoad = async ({ window, getConfig, setConfig, ipc: { send, ha
window.setWindowButtonVisibility?.(!isInPiP);
};
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => {
const blockShortcutsInPiP = (
event: Electron.Event,
input: Electron.Input,
) => {
const key = input.key.toLowerCase();
if (key === 'f') {

View File

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

View File

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

View File

@ -24,21 +24,21 @@
xmlns="http://www.w3.org/2000/svg"
y="0px"
>
<style type="text/css">
.st0 {
<style type="text/css">
.st0 {
fill: #aaaaaa;
}
</style>
</style>
<g id="XMLID_6_">
<path
<path
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
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"
id="XMLID_11_"
/>
</g>
</svg>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"

View File

@ -3,7 +3,8 @@ import { onPlayerApiReady, onUnload } from './renderer';
export default createPlugin({
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,
config: {
enabled: false,
@ -11,5 +12,5 @@ export default createPlugin({
renderer: {
stop: onUnload,
onPlayerApiReady,
}
},
});

View File

@ -29,7 +29,8 @@ const updatePlayBackSpeed = () => {
let menu: Element | null = null;
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)) {
playbackSpeed = 1;
}
@ -38,7 +39,12 @@ const immediateValueChangedListener = (e: Event) => {
};
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 = () => {
@ -49,9 +55,10 @@ const observePopupContainer = () => {
if (
menu &&
(menu.parentElement as HTMLElement & { eventSink_: Element | null })
?.eventSink_
?.matches('ytmusic-menu-renderer.ytmusic-player-bar')&& !menu.contains(slider)
(
menu.parentElement as HTMLElement & { eventSink_: Element | null }
)?.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') &&
!menu.contains(slider)
) {
menu.prepend(slider);
setupSliderListener();
@ -82,14 +89,17 @@ const wheelEventListener = (e: WheelEvent) => {
}
// E.deltaY < 0 means wheel-up
playbackSpeed = roundToTwo(e.deltaY < 0
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
playbackSpeed = roundToTwo(
e.deltaY < 0
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
);
updatePlayBackSpeed();
// 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) {
playbackSpeedSilder.value = playbackSpeed;
}
@ -122,5 +132,10 @@ export const onUnload = () => {
}
slider.removeEventListener('wheel', wheelEventListener);
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"
title="Playback speed"
value="1"
><!--css-build:shady-->
><!--css-build:shady-->
<div class="style-scope tp-yt-paper-slider" id="sliderContainer">
<div class="bar-container style-scope tp-yt-paper-slider">
<tp-yt-paper-progress
@ -41,7 +41,7 @@
role="progressbar"
style="touch-action: none"
value="1"
><!--css-build:shady-->
><!--css-build:shady-->
<div
class="style-scope tp-yt-paper-progress"
@ -61,10 +61,8 @@
</div>
</tp-yt-paper-progress>
</div>
<dom-if class="style-scope tp-yt-paper-slider"
>
<template is="dom-if"></template
>
<dom-if class="style-scope tp-yt-paper-slider">
<template is="dom-if"></template>
</dom-if>
<div
class="slider-knob style-scope tp-yt-paper-slider"
@ -77,11 +75,9 @@
></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>
</dom-if
>
</dom-if>
</tp-yt-paper-slider>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"

View File

@ -21,7 +21,8 @@ export type PreciseVolumePluginConfig = {
export default createPlugin({
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,
config: {
enabled: false,
@ -37,45 +38,76 @@ export default createPlugin({
menu: async ({ setConfig, getConfig, window }) => {
const config = await getConfig();
function changeOptions(changedOptions: Partial<PreciseVolumePluginConfig>, options: PreciseVolumePluginConfig) {
function changeOptions(
changedOptions: Partial<PreciseVolumePluginConfig>,
options: PreciseVolumePluginConfig,
) {
for (const option in changedOptions) {
// 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);
}
// 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) {
const output = await prompt({
title: 'Volume Steps',
label: 'Choose Volume Increase/Decrease Steps',
value: options.steps || 1,
type: 'counter',
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
width: 380,
...promptOptions(),
}, window);
const output = await prompt(
{
title: 'Volume Steps',
label: 'Choose Volume Increase/Decrease Steps',
value: options.steps || 1,
type: 'counter',
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
width: 380,
...promptOptions(),
},
window,
);
if (output || output === 0) { // 0 is somewhat valid
if (output || output === 0) {
// 0 is somewhat valid
changeOptions({ steps: output }, options);
}
}
async function promptGlobalShortcuts(options: PreciseVolumePluginConfig, item: MenuItem) {
const output = await prompt({
title: 'Global Volume Keybinds',
label: 'Choose Global Volume Keybinds:',
type: 'keybind',
keybindOptions: [
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
],
...promptOptions(),
}, window);
async function promptGlobalShortcuts(
options: PreciseVolumePluginConfig,
item: MenuItem,
) {
const output = await prompt(
{
title: 'Global Volume Keybinds',
label: 'Choose Global Volume Keybinds:',
type: 'keybind',
keybindOptions: [
kb(
'Increase Volume',
'volumeUp',
options.globalShortcuts?.volumeUp,
),
kb(
'Decrease Volume',
'volumeDown',
options.globalShortcuts?.volumeDown,
),
],
...promptOptions(),
},
window,
);
if (output) {
const newGlobalShortcuts: {
@ -83,12 +115,15 @@ export default createPlugin({
volumeDown: string;
} = { volumeUp: '', volumeDown: '' };
for (const { value, accelerator } of output) {
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] =
accelerator;
}
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 {
// Reset checkbox if prompt was canceled
item.checked = !item.checked;
@ -107,7 +142,10 @@ export default createPlugin({
{
label: 'Global Hotkeys',
type: 'checkbox',
checked: Boolean(config.globalShortcuts?.volumeUp ?? config.globalShortcuts?.volumeDown),
checked: Boolean(
config.globalShortcuts?.volumeUp ??
config.globalShortcuts?.volumeDown,
),
click: (item) => promptGlobalShortcuts(config, item),
},
{
@ -121,11 +159,15 @@ export default createPlugin({
const config = await getConfig();
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) {
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,
onConfigChange,
}
},
});

View File

@ -13,11 +13,12 @@ function overrideAddEventListener() {
// eslint-disable-next-line @typescript-eslint/unbound-method
Element.prototype._addEventListener = Element.prototype.addEventListener;
// Override addEventListener to Ignore specific events in volume-slider
Element.prototype.addEventListener = function(type: string, listener: (event: Event) => void, useCapture = false) {
if (!(
ignored.id.includes(this.id)
&& ignored.types.includes(type)
)) {
Element.prototype.addEventListener = function (
type: string,
listener: (event: Event) => void,
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
(this as any)._addEventListener(type, listener, useCapture);
} else if (window.electronIs.dev()) {
@ -29,11 +30,16 @@ function overrideAddEventListener() {
export const overrideListener = () => {
overrideAddEventListener();
// Restore original function after finished loading to avoid keeping Element.prototype altered
window.addEventListener('load', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
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
(Element.prototype as any)._addEventListener = undefined;
}, { once: true });
window.addEventListener(
'load',
() => {
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access */
Element.prototype.addEventListener = (
Element.prototype as any
)._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;
export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: RendererContext<PreciseVolumePluginConfig>) => {
export const onPlayerApiReady = async (
playerApi: YoutubePlayer,
context: RendererContext<PreciseVolumePluginConfig>,
) => {
options = await context.getConfig();
api = playerApi;
@ -57,14 +60,20 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
setupLocalArrowShortcuts();
// 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);
if (!noVid) {
setupVideoPlayerOnwheel();
if (!window.mainConfig.plugins.isEnabled('video-toggle')) {
// Video-toggle handles hud positioning on its own
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
$('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode()));
const 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 {
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(
'afterend',
@ -149,8 +159,11 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
// This checks that volume-slider was manually set
const target = mutation.target;
const targetValueNumeric = Number(target.value);
if (mutation.oldValue !== target.value
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) {
if (
mutation.oldValue !== target.value &&
(typeof options.savedVolume !== 'number' ||
Math.abs(options.savedVolume - targetValueNumeric) > 4)
) {
// Diff>4 means it was manually set
setTooltip(targetValueNumeric);
saveVolume(targetValueNumeric);
@ -189,9 +202,11 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
function changeVolume(toIncrease: boolean) {
// Apply volume change if valid
const steps = Number(options.steps || 1);
setVolume(toIncrease
? Math.min(api.getVolume() + steps, 100)
: Math.max(api.getVolume() - steps, 0));
setVolume(
toIncrease
? Math.min(api.getVolume() + steps, 100)
: Math.max(api.getVolume() - steps, 0),
);
}
function updateVolumeSlider() {
@ -200,7 +215,9 @@ export const onPlayerApiReady = async (playerApi: YoutubePlayer, context: Render
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
const silderElement = $<HTMLInputElement>(slider);
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() {
if (options.arrowsShortcut) {
window.addEventListener('keydown', (event) => {
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) {
if (
$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) {
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));
firstRun();

View File

@ -8,6 +8,6 @@
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;
}

View File

@ -9,28 +9,34 @@ import type { YoutubePlayer } from '@/types/youtube-player';
export default createPlugin({
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,
config: {
enabled: false,
},
backend({ ipc, window }) {
ipc.handle('qualityChanger', async (qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox(window, {
type: 'question',
buttons: qualityLabels,
defaultId: currentIndex,
title: 'Choose Video Quality',
message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1,
}));
ipc.handle(
'qualityChanger',
async (qualityLabels: string[], currentIndex: number) =>
await dialog.showMessageBox(window, {
type: 'question',
buttons: qualityLabels,
defaultId: currentIndex,
title: 'Choose Video Quality',
message: 'Choose Video Quality:',
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1,
}),
);
},
renderer: {
qualitySettingsButton: ElementFromHtml(QualitySettingsTemplate),
onPlayerApiReady(api: YoutubePlayer, context) {
const getPlayer = () => document.querySelector<HTMLVideoElement>('#player');
const getPlayer = () =>
document.querySelector<HTMLVideoElement>('#player');
const chooseQuality = () => {
setTimeout(() => getPlayer()?.click());
@ -38,20 +44,27 @@ export default createPlugin({
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
(context.ipc.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex) as Promise<{ response: number }>)
.then((promise) => {
if (promise.response === -1) {
return;
}
(
context.ipc.invoke(
'qualityChanger',
api.getAvailableQualityLabels(),
currentIndex,
) as Promise<{ response: number }>
).then((promise) => {
if (promise.response === -1) {
return;
}
const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality);
});
const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality);
});
};
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);
};
@ -59,8 +72,9 @@ export default createPlugin({
setup();
},
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"
class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings" role="button"
tabindex="0" title="Open player quality changer">
<tp-yt-paper-icon-button
aria-disabled="false"
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">
<svg class="style-scope yt-icon"
focusable="false" preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%;"
viewBox="0 0 24 24">
<svg
class="style-scope yt-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="pointer-events: none; display: block; width: 100%; height: 100%"
viewBox="0 0 24 24"
>
<g class="style-scope yt-icon">
<path
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>
</svg>
</tp-yt-iron-icon>

View File

@ -12,11 +12,12 @@ export type ShortcutsPluginConfig = {
overrideMediaKeys: boolean;
global: ShortcutMappingType;
local: ShortcutMappingType;
}
};
export default createPlugin({
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,
config: {
enabled: false,

View File

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

View File

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

View File

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

View File

@ -21,14 +21,17 @@ function setupMPRIS() {
function registerMPRIS(win: BrowserWindow) {
const songControls = getSongControls(win);
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } =
songControls;
try {
// TODO: "Typing" for this arguments
const secToMicro = (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 seekBy = (o: unknown) => win.webContents.send('seekBy', microToSec(o));
const seekTo = (e: { position: unknown }) =>
win.webContents.send('seekTo', microToSec(e.position));
const seekBy = (o: unknown) =>
win.webContents.send('seekBy', microToSec(o));
const player = setupMPRIS();
@ -42,7 +45,7 @@ function registerMPRIS(win: BrowserWindow) {
ipcMain.on('seeked', (_, t: number) => player.seeked(secToMicro(t)));
let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
ipcMain.on('timeChanged', (_, t: number) => (currentSeconds = t));
ipcMain.on('repeatChanged', (_, mode: string) => {
switch (mode) {
@ -63,7 +66,11 @@ function registerMPRIS(win: BrowserWindow) {
});
player.on('loopStatus', (status: string) => {
// 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 targetIndex = switches.indexOf(status);
@ -91,7 +98,10 @@ function registerMPRIS(win: BrowserWindow) {
}
});
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();
});
@ -106,7 +116,9 @@ function registerMPRIS(win: BrowserWindow) {
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 autoUpdate = false;
@ -166,7 +178,9 @@ function registerMPRIS(win: BrowserWindow) {
player.metadata = data;
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) {

View File

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

View File

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

View File

@ -10,14 +10,22 @@ import type { Segment, SkipSegment } from './types';
export type SponsorBlockPluginConfig = {
enabled: boolean;
apiURL: string;
categories: ('sponsor' | 'intro' | 'outro' | 'interaction' | 'selfpromo' | 'music_offtopic')[];
categories: (
| 'sponsor'
| 'intro'
| 'outro'
| 'interaction'
| 'selfpromo'
| 'music_offtopic'
)[];
};
let currentSegments: Segment[] = [];
export default createPlugin({
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,
config: {
enabled: false,
@ -32,7 +40,11 @@ export default createPlugin({
],
} as SponsorBlockPluginConfig,
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(
categories,
)}`;
@ -48,10 +60,8 @@ export default createPlugin({
return [];
}
const segments = await resp.json() as SkipSegment[];
return sortSegments(
segments.map((submission) => submission.segment),
);
const segments = (await resp.json()) as SkipSegment[];
return sortSegments(segments.map((submission) => submission.segment));
} catch (error) {
if (is.dev()) {
console.log('error on sponsorblock request:', error);
@ -66,7 +76,11 @@ export default createPlugin({
const { apiURL, categories } = config;
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);
});
},
@ -77,8 +91,8 @@ export default createPlugin({
for (const segment of currentSegments) {
if (
target.currentTime >= segment[0]
&& target.currentTime < segment[1]
target.currentTime >= segment[0] &&
target.currentTime < segment[1]
) {
target.currentTime = segment[1];
if (window.electronIs.dev()) {
@ -88,7 +102,7 @@ export default createPlugin({
}
}
},
resetSegments: () => currentSegments = [],
resetSegments: () => (currentSegments = []),
start({ ipc }) {
ipc.on('sponsorblock-skip', (segments: Segment[]) => {
currentSegments = segments;
@ -108,6 +122,6 @@ export default createPlugin({
video.removeEventListener('timeupdate', this.timeUpdateListener);
video.removeEventListener('emptied', this.resetSegments);
}
}
},
},
});

View File

@ -1,12 +1,13 @@
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
UUID: string,
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
actionType: string, // [3]
locked: number, // if submission is locked
votes: number, // Votes on segment
description: string, // title for chapters, empty string for other segments
UUID: string;
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
actionType: string; // [3]
locked: number; // if submission is locked
votes: number; // Votes on segment
description: string; // title for chapters, empty string for other segments
}

View File

@ -37,14 +37,18 @@ export default createPlugin({
click() {
previous();
},
}, {
},
{
tooltip: 'Play/Pause',
// 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() {
playPause();
},
}, {
},
{
tooltip: 'Next',
icon: nativeImage.createFromPath(get('next')),
click() {
@ -81,5 +85,5 @@ export default createPlugin({
window.on('show', () => {
setThumbar(currentSongInfo);
});
}
},
});

View File

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

View File

@ -19,7 +19,7 @@ interface Data {
export default createPlugin({
name: 'Tuna OBS',
description: 'Integration with OBS\'s plugin Tuna',
description: "Integration with OBS's plugin Tuna",
restartNeeded: true,
config: {
enabled: false,
@ -48,18 +48,26 @@ export default createPlugin({
'Access-Control-Allow-Origin': '*',
};
const url = `http://127.0.0.1:${port}/`;
net.fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ data }),
}).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}`);
}
});
net
.fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ data }),
})
.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) => {
if (!this.data.title) {
return;
@ -85,6 +93,6 @@ export default createPlugin({
this.data.album = songInfo.album;
post(this.data);
});
}
}
},
},
});

View File

@ -10,19 +10,23 @@ export interface Plugin<ConfigType extends Config> {
config: ConfigType;
}
export interface RendererPlugin<ConfigType extends Config> extends Plugin<ConfigType> {
export interface RendererPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
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;
}
export interface PreloadPlugin<ConfigType extends Config> extends Plugin<ConfigType> {
export interface PreloadPlugin<ConfigType extends Config>
extends Plugin<ConfigType> {
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;
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
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 {
content: "Video";
content: 'Video';
position: absolute;
top: 0;
bottom: 0;
@ -54,12 +54,16 @@
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);
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;
padding: 15px 0;
display: block;
@ -67,8 +71,10 @@
pointer-events: none;
}
.video-toggle-custom-mode .video-switch-button-checkbox + .video-switch-button-label:before {
content: "";
.video-toggle-custom-mode
.video-switch-button-checkbox
+ .video-switch-button-label:before {
content: '';
background: rgba(60, 60, 60, 0.4);
height: 100%;
width: 100%;

View File

@ -13,11 +13,12 @@ export type VideoTogglePluginConfig = {
mode: 'custom' | 'native' | 'disabled';
forceHide: boolean;
align: 'left' | 'middle' | 'right';
}
};
export default createPlugin({
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,
config: {
enabled: false,
@ -26,10 +27,7 @@ export default createPlugin({
forceHide: false,
align: 'left',
} as VideoTogglePluginConfig,
stylesheets: [
buttonSwitcherStyle,
forceHideStyle,
],
stylesheets: [buttonSwitcherStyle, forceHideStyle],
menu: async ({ getConfig, setConfig }) => {
const config = await getConfig();
@ -124,14 +122,22 @@ export default createPlugin({
switch (config.mode) {
case 'native': {
document.querySelector('ytmusic-player-page')?.setAttribute('has-av-switcher', '');
document.querySelector('ytmusic-player')?.setAttribute('has-av-switcher', '');
document
.querySelector('ytmusic-player-page')
?.setAttribute('has-av-switcher', '');
document
.querySelector('ytmusic-player')
?.setAttribute('has-av-switcher', '');
return;
}
case 'disabled': {
document.querySelector('ytmusic-player-page')?.removeAttribute('has-av-switcher');
document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher');
document
.querySelector('ytmusic-player-page')
?.removeAttribute('has-av-switcher');
document
.querySelector('ytmusic-player')
?.removeAttribute('has-av-switcher');
return;
}
}
@ -140,17 +146,22 @@ export default createPlugin({
const config = await getConfig();
this.config = config;
const moveVolumeHud = window.mainConfig.plugins.isEnabled('precise-volume') ?
preciseVolumeMoveVolumeHud as (_: boolean) => void
: (() => {});
const moveVolumeHud = window.mainConfig.plugins.isEnabled(
'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 switchButtonDiv = ElementFromHtml(buttonTemplate);
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) {
const thumbnail = thumbnails.at(-1)?.url.split('?')[0];
if (thumbnail) img.src = thumbnail;
@ -163,18 +174,28 @@ export default createPlugin({
}
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 (player) {
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>('#song-image')!.style.display = showVideo ? 'none' : 'block';
document.querySelector<HTMLElement>(
'#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) {
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
video.style.top = `${
(player.clientHeight - video.clientHeight) / 2
}px`;
}
moveVolumeHud(showVideo);
@ -182,13 +203,17 @@ export default createPlugin({
};
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
setVideoState(false);
// Hide toggle button
switchButtonDiv.style.display = 'none';
} else {
const songImage = document.querySelector<HTMLImageElement>('#song-image img');
const songImage =
document.querySelector<HTMLImageElement>('#song-image img');
if (!songImage) {
return;
}
@ -197,7 +222,11 @@ export default createPlugin({
// Show toggle button
switchButtonDiv.style.display = 'initial';
// 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);
} else {
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') {
document.querySelector<HTMLVideoElement>('#player')?.prepend(switchButtonDiv);
document
.querySelector<HTMLVideoElement>('#player')
?.prepend(switchButtonDiv);
setVideoState(!config.hideVideo);
forcePlaybackMode();

View File

@ -1,4 +1,4 @@
<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>
</div>

View File

@ -10,24 +10,43 @@ declare module 'butterchurn' {
}
class Visualizer {
constructor(audioContext: AudioContext, canvas: HTMLCanvasElement, opts: ButterchurnOptions);
constructor(
audioContext: AudioContext,
canvas: HTMLCanvasElement,
opts: ButterchurnOptions,
);
loseGLContext(): void;
connectAudio(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>;
createTVars(): Record<string, WebAssembly.Global>;
createPerFramePool(baseVals: unknown): Record<string, WebAssembly.Global>;
createPerPixelPool(baseVals: unknown): Record<string, WebAssembly.Global>;
createCustomShapePerFramePool(baseVals: unknown): 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>;
createCustomShapePerFramePool(
baseVals: unknown,
): 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;
loadPreset(presetMap: unknown, blendTime?: number): Promise<void>;
async loadWASMPreset(preset: unknown, blendTime: number): Promise<void>;
loadJSPreset(preset: unknown, blendTime: number): 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;
setOutputAA(useAA: boolean): void;
setCanvas(canvas: HTMLCanvasElement): void;
@ -44,7 +63,11 @@ declare module '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 {
ButterchurnVisualizer as butterchurn,
VudioVisualizer as vudio,
WaveVisualizer as wave
WaveVisualizer as wave,
} from './visualizers';
type WaveColor = {
@ -19,7 +19,7 @@ export type VisualizerPluginConfig = {
preset: string;
renderingFrequencyInMs: number;
blendTimeInSeconds: number;
},
};
vudio: {
effect: string;
accuracy: number;
@ -35,7 +35,7 @@ export type VisualizerPluginConfig = {
horizontalAlign: string;
verticalAlign: string;
dottify: boolean;
}
};
};
wave: {
animations: {
@ -51,7 +51,7 @@ export type VisualizerPluginConfig = {
lineColor?: string | WaveColor;
radius?: number;
frequencyBand?: string;
}
};
}[];
};
};
@ -151,7 +151,7 @@ export default createPlugin({
const config = await getConfig();
// 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') {
visualizerType = wave;
@ -162,12 +162,15 @@ export default createPlugin({
document.addEventListener(
'audioCanPlay',
(e) => {
const video = document.querySelector<HTMLVideoElement & { captureStream(): MediaStream; }>('video');
const video = document.querySelector<
HTMLVideoElement & { captureStream(): MediaStream }
>('video');
if (!video) {
return;
}
const visualizerContainer = document.querySelector<HTMLElement>('#player');
const visualizerContainer =
document.querySelector<HTMLElement>('#player');
if (!visualizerContainer) {
return;
}
@ -210,7 +213,10 @@ export default createPlugin({
resizeVisualizer(canvas.width, canvas.height);
const visualizerContainerObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
resizeVisualizer(entry.contentRect.width, entry.contentRect.height);
resizeVisualizer(
entry.contentRect.width,
entry.contentRect.height,
);
}
});
visualizerContainerObserver.observe(visualizerContainer);

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Wave } from '@foobar404/wave';
import { Visualizer } from './visualizer';
import type { VisualizerPluginConfig } from '../index';
class WaveVisualizer extends Visualizer<Wave> {
name = 'wave';
@ -32,7 +33,10 @@ class WaveVisualizer extends Visualizer<Wave> {
canvas,
);
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(
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;

View File

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