mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
feat: run prettier
This commit is contained in:
@ -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);
|
||||
|
||||
@ -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({
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 screen’s background.',
|
||||
description:
|
||||
'Applies a lighting effect by casting gentle colors from the video, into your screen’s 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?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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?')) {
|
||||
|
||||
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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>',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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 {',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ import { onRendererLoad } from './renderer';
|
||||
export type LyricsGeniusPluginConfig = {
|
||||
enabled: boolean;
|
||||
romanizedLyrics: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Lyrics Genius',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
|
||||
@ -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 }),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 },
|
||||
);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
9
src/plugins/shortcuts/mpris-service.d.ts
vendored
9
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -17,5 +17,5 @@ export default createPlugin({
|
||||
renderer: {
|
||||
start: onRendererLoad,
|
||||
stop: onRendererUnload,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
37
src/plugins/visualizer/butterchurn.d.ts
vendored
37
src/plugins/visualizer/butterchurn.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -45,8 +45,7 @@ class VudioVisualizer extends Visualizer<Vudio> {
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
}
|
||||
render() {}
|
||||
}
|
||||
|
||||
export default VudioVisualizer;
|
||||
|
||||
@ -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;
|
||||
|
||||
10
src/plugins/visualizer/vudio.d.ts
vendored
10
src/plugins/visualizer/vudio.d.ts
vendored
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user