mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 19:01:47 +00:00
Merge branch 'master' of github.com:th-ch/youtube-music into native-pip
* 'master' of github.com:th-ch/youtube-music: fix PiP hotkey active in searchbox remove titlebar from in-app-menu+PiP Bump "@cliqz/adblocker-electron" version Only run the release stage if it is the main repo Only build without release if it is a fork Use del-cli instead of del (for windows) Replace electron-icon-maker by a more up-to-date fork Adapt CI to yarn v3 Migrate to yarn v3 Track transitioning status Fixed recursive volume changes that caused cpu spike, Switched Repeat Modes to NONE|ONE|ALL Remove references to rimraf Add first version for crossfade plugin removed unnecessary if and used better Repeat change detection connected mpris shuffle, fixed volume, mpris volumes allowed 0.0-1.0 fixed 'repeatChanged' modes being different depending on selected youtube music language fix precise-volume+searchbox interaction fix navbar position
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
module.exports = () => {
|
||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||
require("Simple-YouTube-Age-Restriction-Bypass/Simple-YouTube-Age-Restriction-Bypass.user.js");
|
||||
require("simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js");
|
||||
};
|
||||
|
||||
13
plugins/crossfade/back.js
Normal file
13
plugins/crossfade/back.js
Normal file
@ -0,0 +1,13 @@
|
||||
const { ipcMain } = require("electron");
|
||||
const { Innertube } = require("youtubei.js");
|
||||
|
||||
module.exports = async (win, options) => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
ipcMain.handle("audio-url", async (_, videoID) => {
|
||||
const info = await yt.getBasicInfo(videoID);
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
|
||||
return url;
|
||||
});
|
||||
};
|
||||
360
plugins/crossfade/fader.js
Normal file
360
plugins/crossfade/fader.js
Normal file
@ -0,0 +1,360 @@
|
||||
/**
|
||||
* VolumeFader
|
||||
* Sophisticated Media Volume Fading
|
||||
*
|
||||
* Requires browser support for:
|
||||
* - HTMLMediaElement
|
||||
* - requestAnimationFrame()
|
||||
* - ES6
|
||||
*
|
||||
* Does not depend on any third-party library.
|
||||
*
|
||||
* License: MIT
|
||||
*
|
||||
* Nick Schwarzenberg
|
||||
* v0.2.0, 07/2016
|
||||
*/
|
||||
|
||||
(function (root) {
|
||||
"use strict";
|
||||
|
||||
// internal utility: check if value is a valid volume level and throw if not
|
||||
let validateVolumeLevel = (value) => {
|
||||
// number between 0 and 1?
|
||||
if (!Number.isNaN(value) && value >= 0 && value <= 1) {
|
||||
// yup, that's fine
|
||||
return;
|
||||
} else {
|
||||
// abort and throw an exception
|
||||
throw new TypeError("Number between 0 and 1 expected as volume!");
|
||||
}
|
||||
};
|
||||
|
||||
// main class
|
||||
class VolumeFader {
|
||||
/**
|
||||
* VolumeFader Constructor
|
||||
*
|
||||
* @param media {HTMLMediaElement} - audio or video element to be controlled
|
||||
* @param options {Object} - an object with optional settings
|
||||
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
|
||||
*
|
||||
* options:
|
||||
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
|
||||
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
|
||||
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
|
||||
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
|
||||
*/
|
||||
constructor(media, options) {
|
||||
// passed media element of correct type?
|
||||
if (media instanceof HTMLMediaElement) {
|
||||
// save reference to media element
|
||||
this.media = media;
|
||||
} else {
|
||||
// abort and throw an exception
|
||||
throw new TypeError("Media element expected!");
|
||||
}
|
||||
|
||||
// make sure options is an object
|
||||
options = options || {};
|
||||
|
||||
// log function passed?
|
||||
if (typeof options.logger == "function") {
|
||||
// set log function to the one specified
|
||||
this.logger = options.logger;
|
||||
} else {
|
||||
// set log function explicitly to false
|
||||
this.logger = false;
|
||||
}
|
||||
|
||||
// linear volume fading?
|
||||
if (options.fadeScaling == "linear") {
|
||||
// pass levels unchanged
|
||||
this.scale = {
|
||||
internalToVolume: (level) => level,
|
||||
volumeToInternal: (level) => level,
|
||||
};
|
||||
|
||||
// log setting
|
||||
this.logger && this.logger("Using linear fading.");
|
||||
}
|
||||
// no linear, but logarithmic fading…
|
||||
else {
|
||||
let dynamicRange;
|
||||
|
||||
// default dynamic range?
|
||||
if (
|
||||
options.fadeScaling === undefined ||
|
||||
options.fadeScaling == "logarithmic"
|
||||
) {
|
||||
// set default of 60 dB
|
||||
dynamicRange = 3;
|
||||
}
|
||||
// custom dynamic range?
|
||||
else if (
|
||||
!Number.isNaN(options.fadeScaling) &&
|
||||
options.fadeScaling > 0
|
||||
) {
|
||||
// turn amplitude dB into a multiple of 10 power dB
|
||||
dynamicRange = options.fadeScaling / 2 / 10;
|
||||
}
|
||||
// unsupported value
|
||||
else {
|
||||
// abort and throw exception
|
||||
throw new TypeError(
|
||||
"Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!"
|
||||
);
|
||||
}
|
||||
|
||||
// use exponential/logarithmic scaler for expansion/compression
|
||||
this.scale = {
|
||||
internalToVolume: (level) =>
|
||||
this.exponentialScaler(level, dynamicRange),
|
||||
volumeToInternal: (level) =>
|
||||
this.logarithmicScaler(level, dynamicRange),
|
||||
};
|
||||
|
||||
// log setting if not default
|
||||
options.fadeScaling &&
|
||||
this.logger &&
|
||||
this.logger(
|
||||
"Using logarithmic fading with " +
|
||||
String(10 * dynamicRange) +
|
||||
" dB dynamic range."
|
||||
);
|
||||
}
|
||||
|
||||
// set initial volume?
|
||||
if (options.initialVolume !== undefined) {
|
||||
// validate volume level and throw if invalid
|
||||
validateVolumeLevel(options.initialVolume);
|
||||
|
||||
// set initial volume
|
||||
this.media.volume = options.initialVolume;
|
||||
|
||||
// log setting
|
||||
this.logger &&
|
||||
this.logger(
|
||||
"Set initial volume to " + String(this.media.volume) + "."
|
||||
);
|
||||
}
|
||||
|
||||
// fade duration given?
|
||||
if (options.fadeDuration !== undefined) {
|
||||
// try to set given fade duration (will log if successful and throw if not)
|
||||
this.setFadeDuration(options.fadeDuration);
|
||||
} else {
|
||||
// set default fade duration (1000 ms)
|
||||
this.fadeDuration = 1000;
|
||||
}
|
||||
|
||||
// indicate that fader is not active yet
|
||||
this.active = false;
|
||||
|
||||
// initialization done
|
||||
this.logger && this.logger("Initialized for", this.media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re(start) the update cycle.
|
||||
* (this.active must be truthy for volume updates to take effect)
|
||||
*
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
start() {
|
||||
// set fader to be active
|
||||
this.active = true;
|
||||
|
||||
// start by running the update method
|
||||
this.updateVolume();
|
||||
|
||||
// return instance for chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the update cycle.
|
||||
* (interrupting any fade)
|
||||
*
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
stop() {
|
||||
// set fader to be inactive
|
||||
this.active = false;
|
||||
|
||||
// return instance for chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fade duration.
|
||||
* (used for future calls to fadeTo)
|
||||
*
|
||||
* @param {Number} fadeDuration - fading length in milliseconds
|
||||
* @throws {TypeError} if fadeDuration is not a number greater than zero
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
setFadeDuration(fadeDuration) {
|
||||
// if duration is a valid number > 0…
|
||||
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
|
||||
// set fade duration
|
||||
this.fadeDuration = fadeDuration;
|
||||
|
||||
// log setting
|
||||
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!");
|
||||
}
|
||||
|
||||
// return instance for chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a new fade and start fading.
|
||||
*
|
||||
* @param {Number} targetVolume - level to fade to in the range 0…1
|
||||
* @param {Function} callback - (optional) function to be called when fade is complete
|
||||
* @throws {TypeError} if targetVolume is not in the range 0…1
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
fadeTo(targetVolume, callback) {
|
||||
// validate volume and throw if invalid
|
||||
validateVolumeLevel(targetVolume);
|
||||
|
||||
// define new fade
|
||||
this.fade = {
|
||||
// volume start and end point on internal fading scale
|
||||
volume: {
|
||||
start: this.scale.volumeToInternal(this.media.volume),
|
||||
end: this.scale.volumeToInternal(targetVolume),
|
||||
},
|
||||
// time start and end point
|
||||
time: {
|
||||
start: Date.now(),
|
||||
end: Date.now() + this.fadeDuration,
|
||||
},
|
||||
// optional callback function
|
||||
callback: callback,
|
||||
};
|
||||
|
||||
// start fading
|
||||
this.start();
|
||||
|
||||
// log new fade
|
||||
this.logger && this.logger("New fade started:", this.fade);
|
||||
|
||||
// return instance for chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
// convenience shorthand methods for common fades
|
||||
fadeIn(callback) {
|
||||
this.fadeTo(1, callback);
|
||||
}
|
||||
fadeOut(callback) {
|
||||
this.fadeTo(0, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Update media volume.
|
||||
* (calls itself through requestAnimationFrame)
|
||||
*
|
||||
* @param {Number} targetVolume - linear level to fade to (0…1)
|
||||
* @param {Function} callback - (optional) function to be called when fade is complete
|
||||
*/
|
||||
updateVolume() {
|
||||
// fader active and fade available to process?
|
||||
if (this.active && this.fade) {
|
||||
// get current time
|
||||
let now = Date.now();
|
||||
|
||||
// time left for fading?
|
||||
if (now < this.fade.time.end) {
|
||||
// compute current fade progress
|
||||
let progress =
|
||||
(now - this.fade.time.start) /
|
||||
(this.fade.time.end - this.fade.time.start);
|
||||
|
||||
// compute current level on internal scale
|
||||
let 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);
|
||||
|
||||
// schedule next update
|
||||
root.requestAnimationFrame(this.updateVolume.bind(this));
|
||||
} else {
|
||||
// log end of fade
|
||||
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);
|
||||
|
||||
// set fader to be inactive
|
||||
this.active = false;
|
||||
|
||||
// done, call back (if callable)
|
||||
typeof this.fade.callback == "function" && this.fade.callback();
|
||||
|
||||
// clear fade
|
||||
this.fade = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Exponential scaler with dynamic range limit.
|
||||
*
|
||||
* @param {Number} input - logarithmic input level to be expanded (float, 0…1)
|
||||
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
|
||||
* @return {Number} - expanded level (float, 0…1)
|
||||
*/
|
||||
exponentialScaler(input, dynamicRange) {
|
||||
// special case: make zero (or any falsy input) return zero
|
||||
if (input == 0) {
|
||||
// since the dynamic range is limited,
|
||||
// allow a zero to produce a plain zero instead of a small faction
|
||||
// (audio would not be recognized as silent otherwise)
|
||||
return 0;
|
||||
} else {
|
||||
// scale 0…1 to minus something × 10 dB
|
||||
input = (input - 1) * dynamicRange;
|
||||
|
||||
// compute power of 10
|
||||
return Math.pow(10, input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Logarithmic scaler with dynamic range limit.
|
||||
*
|
||||
* @param {Number} input - exponential input level to be compressed (float, 0…1)
|
||||
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
|
||||
* @return {Number} - compressed level (float, 0…1)
|
||||
*/
|
||||
logarithmicScaler(input, dynamicRange) {
|
||||
// special case: make zero (or any falsy input) return zero
|
||||
if (input == 0) {
|
||||
// logarithm of zero would be -∞, which would map to zero anyway
|
||||
return 0;
|
||||
} else {
|
||||
// compute base-10 logarithm
|
||||
input = Math.log10(input);
|
||||
|
||||
// scale minus something × 10 dB to 0…1 (clipping at 0)
|
||||
return Math.max(1 + input / dynamicRange, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// export class to root scope
|
||||
root.VolumeFader = VolumeFader;
|
||||
})(window);
|
||||
152
plugins/crossfade/front.js
Normal file
152
plugins/crossfade/front.js
Normal file
@ -0,0 +1,152 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { Howl } = require("howler");
|
||||
|
||||
// Extracted from https://github.com/bitfasching/VolumeFader
|
||||
require("./fader");
|
||||
|
||||
let transitionAudio; // Howler audio used to fade out the current music
|
||||
let firstVideo = true;
|
||||
let transitioning = false;
|
||||
|
||||
// Crossfade options that can be overridden in plugin options
|
||||
let crossfadeOptions = {
|
||||
fadeInDuration: 1500, // ms
|
||||
fadeOutDuration: 5000, // ms
|
||||
exitMusicBeforeEnd: 10, // s
|
||||
fadeScaling: "linear",
|
||||
};
|
||||
|
||||
const getStreamURL = async (videoID) => {
|
||||
const url = await ipcRenderer.invoke("audio-url", videoID);
|
||||
return url;
|
||||
};
|
||||
|
||||
const getVideoIDFromURL = (url) => {
|
||||
return new URLSearchParams(url.split("?")?.at(-1)).get("v");
|
||||
};
|
||||
|
||||
const isReadyToCrossfade = () => {
|
||||
return transitionAudio && transitionAudio.state() === "loaded";
|
||||
};
|
||||
|
||||
const watchVideoIDChanges = (cb) => {
|
||||
navigation.addEventListener("navigate", (event) => {
|
||||
const currentVideoID = getVideoIDFromURL(
|
||||
event.currentTarget.currentEntry.url
|
||||
);
|
||||
const nextVideoID = getVideoIDFromURL(event.destination.url);
|
||||
|
||||
if (
|
||||
nextVideoID &&
|
||||
currentVideoID &&
|
||||
(firstVideo || nextVideoID !== currentVideoID)
|
||||
) {
|
||||
if (isReadyToCrossfade()) {
|
||||
crossfade(() => {
|
||||
cb(nextVideoID);
|
||||
});
|
||||
} else {
|
||||
cb(nextVideoID);
|
||||
firstVideo = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createAudioForCrossfade = async (url) => {
|
||||
if (transitionAudio) {
|
||||
transitionAudio.unload();
|
||||
}
|
||||
transitionAudio = new Howl({
|
||||
src: url,
|
||||
html5: true,
|
||||
volume: 0,
|
||||
});
|
||||
await syncVideoWithTransitionAudio();
|
||||
};
|
||||
|
||||
const syncVideoWithTransitionAudio = async () => {
|
||||
const video = document.querySelector("video");
|
||||
const videoFader = new VolumeFader(video, {
|
||||
fadeScaling: crossfadeOptions.fadeScaling,
|
||||
fadeDuration: crossfadeOptions.fadeInDuration,
|
||||
});
|
||||
|
||||
await transitionAudio.play();
|
||||
await transitionAudio.seek(video.currentTime);
|
||||
|
||||
video.onseeking = () => {
|
||||
transitionAudio.seek(video.currentTime);
|
||||
};
|
||||
video.onpause = () => {
|
||||
transitionAudio.pause();
|
||||
};
|
||||
video.onplay = async () => {
|
||||
await transitionAudio.play();
|
||||
await transitionAudio.seek(video.currentTime);
|
||||
|
||||
// Fade in
|
||||
const videoVolume = video.volume;
|
||||
video.volume = 0;
|
||||
videoFader.fadeTo(videoVolume);
|
||||
};
|
||||
|
||||
// Exit just before the end for the transition
|
||||
const transitionBeforeEnd = () => {
|
||||
if (
|
||||
video.currentTime >=
|
||||
video.duration - crossfadeOptions.exitMusicBeforeEnd &&
|
||||
isReadyToCrossfade()
|
||||
) {
|
||||
video.removeEventListener("timeupdate", transitionBeforeEnd);
|
||||
|
||||
// Go to next video - XXX: does not support "repeat 1" mode
|
||||
document.querySelector(".next-button").click();
|
||||
}
|
||||
};
|
||||
video.ontimeupdate = transitionBeforeEnd;
|
||||
};
|
||||
|
||||
const onApiLoaded = () => {
|
||||
watchVideoIDChanges(async (videoID) => {
|
||||
if (!transitioning) {
|
||||
const url = await getStreamURL(videoID);
|
||||
await createAudioForCrossfade(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const crossfade = (cb) => {
|
||||
if (!isReadyToCrossfade()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
transitioning = true;
|
||||
|
||||
const video = document.querySelector("video");
|
||||
|
||||
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
|
||||
initialVolume: video.volume,
|
||||
fadeScaling: crossfadeOptions.fadeScaling,
|
||||
fadeDuration: crossfadeOptions.fadeOutDuration,
|
||||
});
|
||||
|
||||
// Fade out the music
|
||||
video.volume = 0;
|
||||
fader.fadeOut(() => {
|
||||
transitioning = false;
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (options) => {
|
||||
crossfadeOptions = {
|
||||
...crossfadeOptions,
|
||||
options,
|
||||
};
|
||||
|
||||
document.addEventListener("apiLoaded", onApiLoaded, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
};
|
||||
@ -8,8 +8,7 @@
|
||||
#nav-bar-background {
|
||||
opacity: 1 !important;
|
||||
pointer-events: none !important;
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
top: 30px !important;
|
||||
height: 75px !important;
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
const { Menu, app } = require("electron");
|
||||
const { setApplicationMenu } = require("../../../menu");
|
||||
|
||||
module.exports = (win, options, setOptions, togglePip, isInPip) => {
|
||||
if (isInPip) {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
{
|
||||
label: "App",
|
||||
submenu: [
|
||||
{
|
||||
label: "Exit Picture in Picture",
|
||||
click: togglePip,
|
||||
},
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: options.alwaysOnTop,
|
||||
click: (item) => {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
]));
|
||||
} else {
|
||||
setApplicationMenu(win);
|
||||
}
|
||||
};
|
||||
@ -3,7 +3,7 @@ const path = require("path");
|
||||
const { app, ipcMain } = require("electron");
|
||||
const electronLocalshortcut = require("electron-localshortcut");
|
||||
|
||||
const { setOptions, isEnabled } = require("../../config/plugins");
|
||||
const { setOptions } = require("../../config/plugins");
|
||||
const { injectCSS } = require("../utils");
|
||||
|
||||
let isInPiP = false;
|
||||
@ -23,15 +23,6 @@ const setLocalOptions = (_options) => {
|
||||
setOptions("picture-in-picture", _options);
|
||||
}
|
||||
|
||||
|
||||
const adaptors = [];
|
||||
const runAdaptors = () => adaptors.forEach(a => a());
|
||||
|
||||
if (isEnabled("in-app-menu")) {
|
||||
let adaptor = require("./adaptors/in-app-menu");
|
||||
adaptors.push(() => adaptor(win, options, setLocalOptions, togglePiP, isInPiP));
|
||||
}
|
||||
|
||||
const togglePiP = async () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
@ -50,7 +41,6 @@ const togglePiP = async () => {
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", true);
|
||||
|
||||
app.dock?.hide();
|
||||
@ -66,7 +56,6 @@ const togglePiP = async () => {
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
@ -103,9 +92,6 @@ module.exports = (_win, _options) => {
|
||||
ipcMain.on("picture-in-picture", async () => {
|
||||
await togglePiP();
|
||||
});
|
||||
if (options.hotkey) {
|
||||
electronLocalshortcut.register(win, options.hotkey, togglePiP);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setOptions = setLocalOptions;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const { toKeyEvent } = require("keyboardevent-from-electron-accelerator");
|
||||
const keyEventAreEqual = require("keyboardevents-areequal");
|
||||
|
||||
const { getSongMenu } = require("../../providers/dom-elements");
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
@ -71,9 +74,12 @@ const listenForToggle = () => {
|
||||
const player = $('#player');
|
||||
const onPlayerDblClick = player.onDoubleClick_;
|
||||
|
||||
ipcRenderer.on('pip-toggle', (_, isPip) => {
|
||||
const titlebar = $(".cet-titlebar");
|
||||
|
||||
ipcRenderer.on("pip-toggle", (_, isPip) => {
|
||||
if (isPip) {
|
||||
replaceButton(".exit-fullscreen-button", originalExitButton).onclick = () => togglePictureInPicture();
|
||||
replaceButton(".exit-fullscreen-button", originalExitButton).onclick =
|
||||
() => togglePictureInPicture();
|
||||
player.onDoubleClick_ = () => {};
|
||||
expandMenu.onmouseleave = () => middleControls.click();
|
||||
if (!playerPage.playerPageOpen_) {
|
||||
@ -81,12 +87,14 @@ const listenForToggle = () => {
|
||||
}
|
||||
fullScreenButton.click();
|
||||
appLayout.classList.add("pip");
|
||||
if (titlebar) titlebar.style.display = "none";
|
||||
} else {
|
||||
$(".exit-fullscreen-button").replaceWith(originalExitButton);
|
||||
player.onDoubleClick_ = onPlayerDblClick;
|
||||
expandMenu.onmouseleave = undefined;
|
||||
originalExitButton.click();
|
||||
appLayout.classList.remove("pip");
|
||||
if (titlebar) titlebar.style.display = "flex";
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -97,9 +105,9 @@ function observeMenu(options) {
|
||||
"apiLoaded",
|
||||
() => {
|
||||
listenForToggle();
|
||||
// remove native listeners
|
||||
|
||||
cloneButton(".player-minimize-button").onclick = async () => {
|
||||
const isUsingNativePiP = await global.togglePictureInPicture();
|
||||
await global.togglePictureInPicture();
|
||||
setTimeout(() => $("#player").click());
|
||||
};
|
||||
|
||||
@ -115,4 +123,18 @@ function observeMenu(options) {
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = observeMenu;
|
||||
module.exports = (options) => {
|
||||
observeMenu(options);
|
||||
|
||||
if (options.hotkey) {
|
||||
const hotkeyEvent = toKeyEvent(options.hotkey);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
keyEventAreEqual(event, hotkeyEvent) &&
|
||||
!$("ytmusic-search-box").opened
|
||||
) {
|
||||
togglePictureInPicture();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,9 +3,9 @@ ytmusic-app-layout.pip ytmusic-player-bar svg,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .time-info,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
color: white !important;
|
||||
fill: white !important;
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
color: white !important;
|
||||
fill: white !important;
|
||||
}
|
||||
|
||||
/* improve the style of the player bar expanding menu */
|
||||
@ -20,6 +20,23 @@ ytmusic-app-layout.pip ytmusic-player-expanding-menu {
|
||||
top: 22px !important;
|
||||
}
|
||||
|
||||
/* make player-bar not draggable if in-app-menu is enabled */
|
||||
.cet-container ytmusic-app-layout.pip ytmusic-player-bar {
|
||||
-webkit-app-region: no-drag !important;
|
||||
}
|
||||
|
||||
/* make player draggable if in-app-menu is enabled */
|
||||
.cet-container ytmusic-app-layout.pip #player {
|
||||
-webkit-app-region: drag !important;
|
||||
}
|
||||
|
||||
/* remove info, thumbnail and menu from player-bar */
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .content-info-wrapper,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .thumbnail-image-wrapper,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar ytmusic-menu-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* disable the video-toggle button when in PiP mode */
|
||||
ytmusic-app-layout.pip .video-switch-button {
|
||||
display: none !important;
|
||||
|
||||
@ -235,6 +235,7 @@ function setTooltip(volume) {
|
||||
function setupLocalArrowShortcuts() {
|
||||
if (options.arrowsShortcut) {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if ($('ytmusic-search-box').opened) return;
|
||||
switch (event.code) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const mpris = require("mpris-service");
|
||||
const { ipcMain } = require("electron");
|
||||
const {ipcMain} = require("electron");
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const getSongControls = require("../../providers/song-controls");
|
||||
const config = require("../../config");
|
||||
@ -20,7 +20,7 @@ function setupMPRIS() {
|
||||
|
||||
function registerMPRIS(win) {
|
||||
const songControls = getSongControls(win);
|
||||
const { playPause, next, previous, volumeMinus10, volumePlus10 } = songControls;
|
||||
const {playPause, next, previous, volumeMinus10, volumePlus10, shuffle} = songControls;
|
||||
try {
|
||||
const secToMicro = n => Math.round(Number(n) * 1e6);
|
||||
const microToSec = n => Math.round(Number(n) / 1e6);
|
||||
@ -35,33 +35,23 @@ function registerMPRIS(win) {
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||
|
||||
let currentLoopStatus = undefined;
|
||||
let manuallySwitchingStatus = false;
|
||||
ipcMain.on("repeatChanged", (_, mode) => {
|
||||
if (manuallySwitchingStatus)
|
||||
return;
|
||||
|
||||
if (mode === "Repeat off")
|
||||
currentLoopStatus = "None";
|
||||
else if (mode === "Repeat one")
|
||||
currentLoopStatus = "Track";
|
||||
else if (mode === "Repeat all")
|
||||
currentLoopStatus = "Playlist";
|
||||
|
||||
player.loopStatus = currentLoopStatus;
|
||||
if (mode === "NONE")
|
||||
player.loopStatus = mpris.LOOP_STATUS_NONE;
|
||||
else if (mode === "ONE") //MPRIS Playlist and Track Codes are switched to look the same as yt-music icons
|
||||
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
|
||||
else if (mode === "ALL")
|
||||
player.loopStatus = mpris.LOOP_STATUS_TRACK;
|
||||
});
|
||||
player.on("loopStatus", (status) => {
|
||||
// switchRepeat cycles between states in that order
|
||||
const switches = ["None", "Playlist", "Track"];
|
||||
const currentIndex = switches.indexOf(currentLoopStatus);
|
||||
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);
|
||||
|
||||
// Get a delta in the range [0,2]
|
||||
const delta = (targetIndex - currentIndex + 3) % 3;
|
||||
|
||||
manuallySwitchingStatus = true;
|
||||
songControls.switchRepeat(delta);
|
||||
manuallySwitchingStatus = false;
|
||||
})
|
||||
|
||||
player.getPosition = () => secToMicro(currentSeconds)
|
||||
@ -72,19 +62,19 @@ function registerMPRIS(win) {
|
||||
});
|
||||
|
||||
player.on("play", () => {
|
||||
if (player.playbackStatus !== 'Playing') {
|
||||
player.playbackStatus = 'Playing';
|
||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
|
||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
|
||||
playPause()
|
||||
}
|
||||
});
|
||||
player.on("pause", () => {
|
||||
if (player.playbackStatus !== 'Paused') {
|
||||
player.playbackStatus = 'Paused';
|
||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
|
||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
|
||||
playPause()
|
||||
}
|
||||
});
|
||||
player.on("playpause", () => {
|
||||
player.playbackStatus = player.playbackStatus === 'Playing' ? "Paused" : "Playing";
|
||||
player.playbackStatus = player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
|
||||
playPause();
|
||||
});
|
||||
|
||||
@ -94,40 +84,66 @@ function registerMPRIS(win) {
|
||||
player.on('seek', seekBy);
|
||||
player.on('position', seekTo);
|
||||
|
||||
ipcMain.on('volumeChanged', (_, value) => {
|
||||
player.volume = value;
|
||||
player.on('shuffle', (enableShuffle) => {
|
||||
shuffle();
|
||||
});
|
||||
player.on('volume', (newVolume) => {
|
||||
if (config.plugins.isEnabled('precise-volume')) {
|
||||
// With precise volume we can set the volume to the exact value.
|
||||
win.webContents.send('setVolume', newVolume)
|
||||
} else {
|
||||
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
|
||||
const deltaVolume = Math.round((newVolume - player.volume) / 10);
|
||||
|
||||
if (deltaVolume > 0) {
|
||||
for (let i = 0; i < deltaVolume; i++)
|
||||
volumePlus10();
|
||||
let mprisVolNewer = false;
|
||||
let autoUpdate = false;
|
||||
ipcMain.on('volumeChanged', (_, newVol) => {
|
||||
if (parseInt(player.volume * 100) !== newVol) {
|
||||
if (mprisVolNewer) {
|
||||
mprisVolNewer = false;
|
||||
autoUpdate = false;
|
||||
} else {
|
||||
for (let i = 0; i < -deltaVolume; i++)
|
||||
volumeMinus10();
|
||||
autoUpdate = true;
|
||||
player.volume = parseFloat((newVol / 100).toFixed(2));
|
||||
mprisVolNewer = false;
|
||||
autoUpdate = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerCallback(songInfo => {
|
||||
if (player) {
|
||||
const data = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
'mpris:artUrl': songInfo.imageSrc,
|
||||
'xesam:title': songInfo.title,
|
||||
'xesam:artist': [songInfo.artist],
|
||||
player.on('volume', (newVolume) => {
|
||||
if (config.plugins.isEnabled('precise-volume')) {
|
||||
// With precise volume we can set the volume to the exact value.
|
||||
let newVol = parseInt(newVolume * 100);
|
||||
if (parseInt(player.volume * 100) !== newVol) {
|
||||
if (!autoUpdate){
|
||||
mprisVolNewer = true;
|
||||
autoUpdate = false;
|
||||
win.webContents.send('setVolume', newVol);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
|
||||
let deltaVolume = Math.round((newVolume - player.volume) * 10);
|
||||
while (deltaVolume !== 0 && deltaVolume > 0) {
|
||||
volumePlus10();
|
||||
player.volume = player.volume + 0.1;
|
||||
deltaVolume--;
|
||||
}
|
||||
while (deltaVolume !== 0 && deltaVolume < 0) {
|
||||
volumeMinus10();
|
||||
player.volume = player.volume - 0.1;
|
||||
deltaVolume++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerCallback(songInfo => {
|
||||
if (player) {
|
||||
const data = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
'mpris:artUrl': songInfo.imageSrc,
|
||||
'xesam:title': songInfo.title,
|
||||
'xesam:artist': [songInfo.artist],
|
||||
'mpris:trackid': '/'
|
||||
};
|
||||
if (songInfo.album) data['xesam:album'] = songInfo.album;
|
||||
player.metadata = data;
|
||||
player.seeked(secToMicro(songInfo.elapsedSeconds))
|
||||
player.playbackStatus = songInfo.isPaused ? "Paused" : "Playing"
|
||||
player.seeked(secToMicro(songInfo.elapsedSeconds));
|
||||
player.playbackStatus = songInfo.isPaused ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user