Merge branch 'local-upstream/master' into new-downloader

This commit is contained in:
Araxeus
2023-03-19 20:57:55 +02:00
41 changed files with 930 additions and 385 deletions

View File

@ -11,28 +11,32 @@ module.exports = (options) => {
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
}
/**
* If captions are disabled by default,
* unload "captions" module when video changes.
*/
const videoChanged = (api, options) => {
if (options.disableCaptions) {
setTimeout(() => api.unloadModule("captions"), 100);
}
}
function setup(event, options) {
const api = event.detail;
$("video").addEventListener("srcChanged", () => videoChanged(api, options));
$(".right-controls-buttons").append(captionsSettingsButton);
let captionTrackList = api.getOption("captions", "tracklist");
$("video").addEventListener("srcChanged", () => {
if (options.disableCaptions) {
setTimeout(() => api.unloadModule("captions"), 100);
captionsSettingsButton.style.display = "none";
return;
}
api.loadModule("captions");
setTimeout(() => {
captionTrackList = api.getOption("captions", "tracklist");
captionsSettingsButton.style.display = captionTrackList?.length
? "inline-block"
: "none";
}, 250);
});
captionsSettingsButton.onclick = async () => {
api.loadModule("captions");
const captionTrackList = api.getOption("captions", "tracklist");
if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption("captions", "track");
let currentIndex = !currentCaptionTrack ?

View File

@ -2,14 +2,12 @@ const path = require("path");
const electronLocalshortcut = require("electron-localshortcut");
const config = require("../../config");
const { injectCSS } = require("../utils");
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
setupTitlebar();
//tracks menu visibility
let visible = !config.get("options.hideMenu");
module.exports = (win) => {
// css for custom scrollbar + disable drag area(was causing bugs)
@ -18,16 +16,8 @@ module.exports = (win) => {
win.once("ready-to-show", () => {
attachTitlebarToWindow(win);
//register keyboard shortcut && hide menu if hideMenu is enabled
if (config.get("options.hideMenu")) {
electronLocalshortcut.register(win, "Esc", () => {
setMenuVisibility(!visible);
});
}
electronLocalshortcut.register(win, "`", () => {
win.webContents.send("toggleMenu");
});
});
function setMenuVisibility(value) {
visible = value;
win.webContents.send("refreshMenu", visible);
}
};

View File

@ -5,28 +5,31 @@ const { isEnabled } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); }
module.exports = (options) => {
let visible = !config.get("options.hideMenu");
let visible = () => !!$('.cet-menubar').firstChild;
const bar = new Titlebar({
icon: "https://cdn-icons-png.flaticon.com/512/5358/5358672.png",
backgroundColor: Color.fromHex("#050505"),
itemBackgroundColor: Color.fromHex("#1d1d1d"),
svgColor: Color.WHITE,
menu: visible ? undefined : null
menu: config.get("options.hideMenu") ? null : undefined
});
bar.updateTitle(" ");
document.title = "Youtube Music";
const hideIcon = hide => $('.cet-window-icon').style.display = hide ? 'none' : 'flex';
if (options.hideIcon) hideIcon(true);
ipcRenderer.on("refreshMenu", (_, showMenu) => {
if (showMenu === undefined && !visible) return;
if (showMenu === false) {
const toggleMenu = () => {
if (visible()) {
bar.updateMenu(null);
visible = false;
} else {
bar.refreshMenu();
visible = true;
}
};
$('.cet-window-icon').addEventListener('click', toggleMenu);
ipcRenderer.on("toggleMenu", toggleMenu);
ipcRenderer.on("refreshMenu", () => {
if (visible()) {
bar.refreshMenu();
}
});
@ -36,14 +39,13 @@ module.exports = (options) => {
});
}
ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => {
setNavbarMargin();
const playPageObserver = new MutationObserver(setNavbarMargin);
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
setupSearchOpenObserver();
setupMenuOpenObserver();
}, { once: true, passive: true })
};
@ -55,6 +57,15 @@ function setupSearchOpenObserver() {
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ["opened"] })
}
function setupMenuOpenObserver() {
const menuOpenObserver = new MutationObserver(mutations => {
$('#nav-bar-background').style.webkitAppRegion =
Array.from($('.cet-menubar').childNodes).some(c => c.classList.contains('open')) ?
'no-drag' : 'drag';
});
menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ["class"] })
}
function setNavbarMargin() {
$('#nav-bar-background').style.right =
$('ytmusic-app-layout').playerPageOpen_ ?

View File

@ -1,14 +0,0 @@
const { setOptions } = require("../../config/plugins");
module.exports = (win, options) => [
{
label: "Hide Icon",
type: "checkbox",
checked: options.hideIcon,
click: (item) => {
win.webContents.send("hideIcon", item.checked);
options.hideIcon = item.checked;
setOptions("in-app-menu", options);
},
}
];

View File

@ -12,11 +12,17 @@
height: 75px !important;
}
/* fixes top gap between nav-bar and browse-page */
/* fix top gap between nav-bar and browse-page */
#browse-page {
padding-top: 0 !important;
}
/* fix navbar hiding library items */
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"] {
top: 50px;
position: relative;
}
/* remove window dragging for nav bar (conflict with titlebar drag) */
ytmusic-nav-bar,
.tab-titleiron-icon,
@ -46,7 +52,7 @@ yt-page-navigation-progress,
top: 30px !important;
}
/* Custom scrollbar */
/* custom scrollbar */
::-webkit-scrollbar {
width: 12px;
background-color: #030303;
@ -59,7 +65,7 @@ yt-page-navigation-progress,
background-color: rgba(15, 15, 15, 0.699);
}
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */
::-webkit-scrollbar-thumb:vertical {
border: 2px solid rgba(0, 0, 0, 0);
@ -70,7 +76,7 @@ yt-page-navigation-progress,
-webkit-border-radius: 100px;
}
::-webkit-scrollbar-thumb:vertical:active {
background: #4d4c4c; /* Some darker color when you click it */
background: #4d4c4c; /* some darker color when you click it */
border-radius: 100px;
-moz-border-radius: 100px;
-webkit-border-radius: 100px;
@ -80,6 +86,17 @@ yt-page-navigation-progress,
background-color: inherit
}
/** hideMenu toggler **/
.cet-window-icon {
-webkit-app-region: no-drag;
}
.cet-window-icon img {
-webkit-user-drag: none;
filter: invert(50%);
}
/** make navbar draggable **/
#nav-bar-background {
-webkit-app-region: drag;
}

View File

@ -7,8 +7,13 @@ const fetch = require("node-fetch");
const { cleanupName } = require("../../providers/song-info");
const { injectCSS } = require("../utils");
let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false;
module.exports = async (win) => {
module.exports = async (win, options) => {
if(options.romanizedLyrics) {
revRomanized = true;
}
injectCSS(win.webContents, join(__dirname, "style.css"));
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
@ -17,17 +22,51 @@ module.exports = async (win) => {
});
};
const toggleRomanized = () => {
revRomanized = !revRomanized;
};
const fetchFromGenius = async (metadata) => {
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
metadata.title
)}`;
const songTitle = `${cleanupName(metadata.title)}`;
const songArtist = `${cleanupName(metadata.artist)}`;
let lyrics;
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
Genius Lyrics behavior is observed.
*/
let hasAsianChars = false;
if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) {
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
hasAsianChars = true;
} else {
lyrics = await getLyricsList(`${songArtist} ${songTitle}`);
}
/* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
*/
if(revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) {
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
}
return lyrics;
};
/**
* Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song
* @param {*} queryString
* @returns The lyrics of the first song found using the Genius-Lyrics API
*/
const getLyricsList = async (queryString) => {
let response = await fetch(
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(queryString)}`
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`
);
if (!response.ok) {
return 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();
let url = "";
try {
@ -36,16 +75,23 @@ const fetchFromGenius = async (metadata) => {
} catch {
return null;
}
let lyrics = await getLyrics(url);
return lyrics;
}
if (is.dev()) {
console.log("Fetching lyrics from Genius:", url);
}
/**
*
* @param {*} url
* @returns The lyrics of the song URL provided, null if none
*/
const getLyrics = async (url) => {
response = await fetch(url);
if (!response.ok) {
return null;
}
if (is.dev()) {
console.log("Fetching lyrics from Genius:", url);
}
const html = await response.text();
const lyrics = convert(html, {
baseElements: {
@ -64,8 +110,8 @@ const fetchFromGenius = async (metadata) => {
},
},
});
return lyrics;
};
module.exports.fetchFromGenius = fetchFromGenius;
module.exports.toggleRomanized = toggleRomanized;
module.exports.fetchFromGenius = fetchFromGenius;

View File

@ -2,7 +2,7 @@ const { ipcRenderer } = require("electron");
const is = require("electron-is");
module.exports = () => {
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => setTimeout(() => {
const tabList = document.querySelectorAll("tp-yt-paper-tab");
const tabs = {
upNext: tabList[0],
@ -90,5 +90,5 @@ module.exports = () => {
tabs.lyrics.removeAttribute("disabled");
tabs.lyrics.removeAttribute("aria-disabled");
}
});
}, 500));
};

View File

@ -0,0 +1,17 @@
const { setOptions } = require("../../config/plugins");
const { toggleRomanized } = require("./back");
module.exports = (win, options, refreshMenu) => {
return [
{
label: "Romanized Lyrics",
type: "checkbox",
checked: options.romanizedLyrics,
click: (item) => {
options.romanizedLyrics = item.checked;
setOptions('lyrics-genius', options);
toggleRomanized();
},
},
];
};

View File

@ -2,8 +2,9 @@ const { Notification } = require("electron");
const is = require("electron-is");
const registerCallback = require("../../providers/song-info");
const { notificationImage } = require("./utils");
const config = require("./config");
const notify = (info, options) => {
const notify = (info) => {
// Fill the notification with content
const notification = {
@ -11,7 +12,7 @@ const notify = (info, options) => {
body: info.artist,
icon: notificationImage(info),
silent: true,
urgency: options.urgency,
urgency: config.get('urgency'),
};
// Send the notification
@ -21,24 +22,25 @@ const notify = (info, options) => {
return currentNotification;
};
const setup = (options) => {
const setup = () => {
let oldNotification;
let currentUrl;
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) {
// Close the old notification
oldNotification?.close();
currentUrl = songInfo.url;
// This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
setTimeout(() => { oldNotification = notify(songInfo) }, 10);
}
});
}
/** @param {Electron.BrowserWindow} win */
module.exports = (win, options) => {
// Register the callback for new song information
is.windows() && options.interactive ?
require("./interactive")(win, options.unpauseNotification) :
setup(options);
require("./interactive")(win) :
setup();
};

View File

@ -0,0 +1,5 @@
const { PluginConfig } = require("../../config/dynamic");
const config = new PluginConfig("notifications");
module.exports = { ...config };

View File

@ -1,106 +1,235 @@
const { notificationImage, icons } = require("./utils");
const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require("./utils");
const getSongControls = require('../../providers/song-controls');
const registerCallback = require("../../providers/song-info");
const is = require("electron-is");
const WindowsToaster = require('node-notifier').WindowsToaster;
const { changeProtocolHandler } = require("../../providers/protocol-handler");
const { setTrayOnClick, setTrayOnDoubleClick } = require("../../tray");
const notifier = new WindowsToaster({ withFallback: true });
const { Notification, app, ipcMain } = require("electron");
const path = require('path');
//store song controls reference on launch
let controls;
let notificationOnUnpause;
const config = require("./config");
module.exports = (win, unpauseNotification) => {
//Save controls and onPause option
const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };
notificationOnUnpause = unpauseNotification;
let songControls;
let savedNotification;
let currentUrl;
/** @param {Electron.BrowserWindow} win */
module.exports = (win) => {
songControls = getSongControls(win);
let currentSeconds = 0;
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
if (app.isPackaged) save_temp_icons();
let savedSongInfo;
let lastUrl;
// Register songInfoCallback
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
currentUrl = songInfo.url;
sendToaster(songInfo);
if (!songInfo.artist && !songInfo.title) return;
savedSongInfo = { ...songInfo };
if (!songInfo.isPaused &&
(songInfo.url !== lastUrl || config.get("unpauseNotification"))
) {
lastUrl = songInfo.url
sendNotification(songInfo);
}
});
win.webContents.once("closed", () => {
deleteNotification()
if (config.get("trayControls")) {
setTrayOnClick(() => {
if (savedNotification) {
savedNotification.close();
savedNotification = undefined;
} else if (savedSongInfo) {
sendNotification({
...savedSongInfo,
elapsedSeconds: currentSeconds
})
}
});
setTrayOnDoubleClick(() => {
if (win.isVisible()) {
win.hide();
} else win.show();
})
}
app.once("before-quit", () => {
savedNotification?.close();
});
changeProtocolHandler(
(cmd) => {
if (Object.keys(songControls).includes(cmd)) {
songControls[cmd]();
if (config.get("refreshOnPlayPause") && (
cmd === 'pause' ||
(cmd === 'play' && !config.get("unpauseNotification"))
)
) {
setImmediate(() =>
sendNotification({
...savedSongInfo,
isPaused: cmd === 'pause',
elapsedSeconds: currentSeconds
})
);
}
}
}
)
}
//delete old notification
let toDelete;
function deleteNotification() {
if (toDelete !== undefined) {
// To remove the notification it has to be done this way
const removeNotif = Object.assign(toDelete, {
remove: toDelete.id
})
notifier.notify(removeNotif)
function sendNotification(songInfo) {
const iconSrc = notificationImage(songInfo);
toDelete = undefined;
savedNotification?.close();
savedNotification = new Notification({
title: songInfo.title || "Playing",
body: songInfo.artist,
icon: iconSrc,
silent: true,
// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
toastXml: get_xml(songInfo, iconSrc),
});
savedNotification.on("close", (_) => {
savedNotification = undefined;
});
savedNotification.show();
}
const get_xml = (songInfo, iconSrc) => {
switch (config.get("toastStyle")) {
default:
case ToastStyles.logo:
case ToastStyles.legacy:
return xml_logo(songInfo, iconSrc);
case ToastStyles.banner_top_custom:
return xml_banner_top_custom(songInfo, iconSrc);
case ToastStyles.hero:
return xml_hero(songInfo, iconSrc);
case ToastStyles.banner_bottom:
return xml_banner_bottom(songInfo, iconSrc);
case ToastStyles.banner_centered_bottom:
return xml_banner_centered_bottom(songInfo, iconSrc);
case ToastStyles.banner_centered_top:
return xml_banner_centered_top(songInfo, iconSrc);
};
}
const iconLocation = app.isPackaged ?
path.resolve(app.getPath("userData"), 'icons') :
path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
const display = (kind) => {
if (config.get("toastStyle") === ToastStyles.legacy) {
return `content="${icons[kind]}"`;
} else {
return `\
content="${config.get("hideButtonText") ? "" : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
`;
}
}
//New notification
function sendToaster(songInfo) {
deleteNotification();
//download image and get path
let imgSrc = notificationImage(songInfo, true);
toDelete = {
appID: "com.github.th-ch.youtube-music",
title: songInfo.title || "Playing",
message: songInfo.artist,
id: parseInt(Math.random() * 1000000, 10),
icon: imgSrc,
actions: [
icons.previous,
songInfo.isPaused ? icons.play : icons.pause,
icons.next
],
sound: false,
};
//send notification
notifier.notify(
toDelete,
(err, data) => {
// Will also wait until notification is closed.
if (err) {
console.log(`ERROR = ${err.toString()}\n DATA = ${data}`);
}
switch (data) {
//buttons
case icons.previous.normalize():
controls.previous();
return;
case icons.next.normalize():
controls.next();
return;
case icons.play.normalize():
controls.playPause();
// dont delete notification on play/pause
toDelete = undefined;
//manually send notification if not sending automatically
if (!notificationOnUnpause) {
songInfo.isPaused = false;
sendToaster(songInfo);
}
return;
case icons.pause.normalize():
controls.playPause();
songInfo.isPaused = true;
toDelete = undefined;
sendToaster(songInfo);
return;
//Native datatype
case "dismissed":
case "timeout":
deleteNotification();
}
}
const getButton = (kind) =>
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
);
const getButtons = (isPaused) => `\
<actions>
${getButton('previous')}
${isPaused ? getButton('play') : getButton('pause')}
${getButton('next')}
</actions>\
`;
const toast = (content, isPaused) => `\
<toast>
<audio silent="true" />
<visual>
<binding template="ToastGeneric">
${content}
</binding>
</visual>
${getButtons(isPaused)}
</toast>`;
const xml_image = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
<text id="1">${title}</text>
<text id="2">${artist}</text>\
`, isPaused);
const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"');
const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"');
const xml_banner_bottom = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, '');
const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
<image id="1" src="${imgSrc}" name="Image" />
<text></text>
<group>
<subgroup>
<text hint-style="body">${songInfo.title}</text>
<text hint-style="captionSubtle">${songInfo.artist}</text>
</subgroup>
${xml_more_data(songInfo)}
</group>\
`, songInfo.isPaused);
const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\
<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)} / ${secondsToMinutes(songDuration)}</text>
</subgroup>\
`;
const xml_banner_centered_bottom = ({ title, artist, isPaused }, imgSrc) => 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="SubtitleSubtle">${artist}</text>
</subgroup>
</group>
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
`, isPaused);
const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => 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="SubtitleSubtle">${artist}</text>
</subgroup>
</group>\
`, isPaused);
const titleFontPicker = (title) => {
if (title.length <= 13) {
return 'Header';
} else if (title.length <= 22) {
return 'Subheader';
} else if (title.length <= 26) {
return 'Title';
} else {
return 'Subtitle';
}
}

View File

@ -1,30 +1,80 @@
const { urgencyLevels, setOption } = require("./utils");
const { urgencyLevels, ToastStyles, snakeToCamel } = require("./utils");
const is = require("electron-is");
const config = require("./config");
module.exports = (win, options) => [
...(is.linux() ?
[{
label: "Notification Priority",
submenu: urgencyLevels.map(level => ({
label: level.name,
type: "radio",
checked: options.urgency === level.value,
click: () => setOption(options, "urgency", level.value)
})),
}] :
[]),
...(is.windows() ?
[{
label: "Interactive Notifications",
type: "checkbox",
checked: options.interactive,
click: (item) => setOption(options, "interactive", item.checked)
}] :
[]),
module.exports = (_win, options) => [
...(is.linux()
? [
{
label: "Notification Priority",
submenu: urgencyLevels.map((level) => ({
label: level.name,
type: "radio",
checked: options.urgency === level.value,
click: () => config.set("urgency", level.value),
})),
},
]
: []),
...(is.windows()
? [
{
label: "Interactive Notifications",
type: "checkbox",
checked: options.interactive,
// doesn't update until restart
click: (item) => config.setAndMaybeRestart("interactive", item.checked),
},
{
// submenu with settings for interactive notifications (name shouldn't be too long)
label: "Interactive Settings",
submenu: [
{
label: "Open/Close on tray click",
type: "checkbox",
checked: options.trayControls,
click: (item) => config.set("trayControls", item.checked),
},
{
label: "Hide Button Text",
type: "checkbox",
checked: options.hideButtonText,
click: (item) => config.set("hideButtonText", item.checked),
},
{
label: "Refresh on Play/Pause",
type: "checkbox",
checked: options.refreshOnPlayPause,
click: (item) => config.set("refreshOnPlayPause", item.checked),
}
]
},
{
label: "Style",
submenu: getToastStyleMenuItems(options)
},
]
: []),
{
label: "Show notification on unpause",
type: "checkbox",
checked: options.unpauseNotification,
click: (item) => setOption(options, "unpauseNotification", item.checked)
click: (item) => config.set("unpauseNotification", item.checked),
},
];
function getToastStyleMenuItems(options) {
const arr = new Array(Object.keys(ToastStyles).length);
// ToastStyles index starts from 1
for (const [name, index] of Object.entries(ToastStyles)) {
arr[index - 1] = {
label: snakeToCamel(name),
type: "radio",
checked: options.toastStyle === index,
click: () => config.set("toastStyle", index),
};
}
return arr;
}

View File

@ -1,10 +1,22 @@
const { setMenuOptions } = require("../../config/plugins");
const path = require("path");
const { app } = require("electron");
const fs = require("fs");
const config = require("./config");
const icon = "assets/youtube-music.png";
const tempIcon = path.join(app.getPath("userData"), "tempIcon.png");
const userData = app.getPath("userData");
const tempIcon = path.join(userData, "tempIcon.png");
const tempBanner = path.join(userData, "tempBanner.png");
module.exports.ToastStyles = {
logo: 1,
banner_centered_top: 2,
hero: 3,
banner_top_custom: 4,
banner_centered_bottom: 5,
banner_bottom: 6,
legacy: 7
}
module.exports.icons = {
play: "\u{1405}", // ᐅ
@ -13,38 +25,37 @@ module.exports.icons = {
previous: "\u{1438}" //
}
module.exports.setOption = (options, option, value) => {
options[option] = value;
setMenuOptions("notifications", options)
}
module.exports.urgencyLevels = [
{ name: "Low", value: "low" },
{ name: "Normal", value: "normal" },
{ name: "High", value: "critical" },
];
module.exports.notificationImage = function (songInfo, saveIcon = false) {
//return local path to temp icon
if (saveIcon && !!songInfo.image) {
try {
fs.writeFileSync(tempIcon,
centerNativeImage(songInfo.image)
.toPNG()
);
} catch (err) {
console.log(`Error writing song icon to disk:\n${err.toString()}`)
return icon;
}
return tempIcon;
}
//else: return image
return songInfo.image
? centerNativeImage(songInfo.image)
: icon
module.exports.notificationImage = (songInfo) => {
if (!songInfo.image) return icon;
if (!config.get("interactive")) return nativeImageToLogo(songInfo.image);
switch (config.get("toastStyle")) {
case module.exports.ToastStyles.logo:
case module.exports.ToastStyles.legacy:
return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon);
default:
return this.saveImage(songInfo.image, tempBanner);
};
};
function centerNativeImage(nativeImage) {
module.exports.saveImage = (img, save_path) => {
try {
fs.writeFileSync(save_path, img.toPNG());
} catch (err) {
console.log(`Error writing song icon to disk:\n${err.toString()}`)
return icon;
}
return save_path;
}
function nativeImageToLogo(nativeImage) {
const tempImage = nativeImage.resize({ height: 256 });
const margin = Math.max((tempImage.getSize().width - 256), 0);
@ -54,3 +65,27 @@ function centerNativeImage(nativeImage) {
width: 256, height: 256
})
}
module.exports.save_temp_icons = () => {
for (const kind of Object.keys(module.exports.icons)) {
const destinationPath = path.join(userData, 'icons', `${kind}.png`);
if (fs.existsSync(destinationPath)) continue;
const iconPath = path.resolve(__dirname, "../../assets/media-icons-black", `${kind}.png`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFile(iconPath, destinationPath, () => { });
}
};
module.exports.snakeToCamel = (str) => {
return str.replace(/([-_][a-z]|^[a-z])/g, (group) =>
group.toUpperCase()
.replace('-', ' ')
.replace('_', ' ')
);
}
module.exports.secondsToMinutes = (seconds) => {
const minutes = Math.floor(seconds / 60);
const secondsLeft = seconds % 60;
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
}

View File

@ -34,7 +34,7 @@ const observer = new MutationObserver(() => {
menu = getSongMenu();
if (!menu) return;
}
if (menu.contains(pipButton)) return;
if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) return;
const menuUrl = $(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
)?.href;

View File

@ -30,7 +30,7 @@ const observePopupContainer = () => {
menu = getSongMenu();
}
if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) {
if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) {
menu.prepend(slider);
if (!observingSlider) {
setupSliderListener();

View File

@ -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");
@ -18,9 +18,10 @@ function setupMPRIS() {
return player;
}
/** @param {Electron.BrowserWindow} win */
function registerMPRIS(win) {
const songControls = getSongControls(win);
const {playPause, next, previous, volumeMinus10, volumePlus10, shuffle} = 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);
@ -30,6 +31,13 @@ function registerMPRIS(win) {
const player = setupMPRIS();
ipcMain.on("apiLoaded", () => {
win.webContents.send("setupSeekedListener", "mpris");
win.webContents.send("setupTimeChangedListener", "mpris");
win.webContents.send("setupRepeatChangedListener", "mpris");
win.webContents.send("setupVolumeChangedListener", "mpris");
});
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
let currentSeconds = 0;
@ -109,7 +117,7 @@ function registerMPRIS(win) {
// 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){
if (!autoUpdate) {
mprisVolNewer = true;
autoUpdate = false;
win.webContents.send('setVolume', newVol);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

View File

@ -32,22 +32,22 @@ function setThumbar(win, songInfo) {
win.setThumbarButtons([
{
tooltip: 'Previous',
icon: get('backward.png'),
icon: get('previous'),
click() { controls.previous(win.webContents); }
}, {
tooltip: 'Play/Pause',
// Update icon based on play state
icon: songInfo.isPaused ? get('play.png') : get('pause.png'),
icon: songInfo.isPaused ? get('play') : get('pause'),
click() { controls.playPause(win.webContents); }
}, {
tooltip: 'Next',
icon: get('forward.png'),
icon: get('next'),
click() { controls.next(win.webContents); }
}
]);
}
// Util
function get(file) {
return path.join(__dirname, "assets", file);
function get(kind) {
return path.join(__dirname, "../../assets/media-icons-black", `${kind}.png`);
}

View File

@ -28,7 +28,9 @@ const post = async (data) => {
fetch(url, { method: 'POST', headers, body: JSON.stringify({ data }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
}
/** @param {Electron.BrowserWindow} win */
module.exports = async (win) => {
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
ipcMain.on('timeChanged', async (_, t) => {
if (!data.title) return;
data.progress = secToMilisec(t);