mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
Interactive Notifications v2
This commit is contained in:
@ -55,8 +55,10 @@ const defaultConfig = {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: "normal", //has effect only on Linux
|
||||
interactive: true, //has effect only on Windows
|
||||
smallInteractive: false //has effect only on Windows
|
||||
// the following has effect only on Windows
|
||||
interactive: true,
|
||||
toastStyle: 1, // see plugins/notifications/utils for more info
|
||||
hideButtonText: false
|
||||
},
|
||||
"precise-volume": {
|
||||
enabled: false,
|
||||
|
||||
1
index.js
1
index.js
@ -75,6 +75,7 @@ function onClosed() {
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
/** @param {Electron.BrowserWindow} win */
|
||||
function loadPlugins(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||
// Load user CSS
|
||||
|
||||
@ -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,26 @@ 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) => {
|
||||
config.init(options);
|
||||
// Register the callback for new song information
|
||||
is.windows() && options.interactive ?
|
||||
require("./interactive")(win, options) :
|
||||
setup(options);
|
||||
require("./interactive")(win) :
|
||||
setup();
|
||||
};
|
||||
|
||||
22
plugins/notifications/config.js
Normal file
22
plugins/notifications/config.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { setOptions, setMenuOptions } = require("../../config/plugins");
|
||||
|
||||
let config;
|
||||
|
||||
module.exports.init = (options) => {
|
||||
config = options;
|
||||
}
|
||||
|
||||
module.exports.setAndMaybeRestart = (option, value) => {
|
||||
config[option] = value;
|
||||
setMenuOptions("notifications", config);
|
||||
}
|
||||
|
||||
module.exports.set = (option, value) => {
|
||||
config[option] = value;
|
||||
setOptions("notifications", config);
|
||||
}
|
||||
|
||||
module.exports.get = (option) => {
|
||||
let res = config[option];
|
||||
return res;
|
||||
}
|
||||
@ -1,31 +1,38 @@
|
||||
const { notificationImage, icons, save_temp_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 { changeProtocolHandler } = require("../../providers/protocol-handler");
|
||||
|
||||
const { Notification, app } = require("electron");
|
||||
const { Notification, app, ipcMain } = require("electron");
|
||||
const path = require('path');
|
||||
|
||||
let songControls;
|
||||
let config;
|
||||
let savedNotification;
|
||||
const config = require("./config");
|
||||
|
||||
module.exports = (win, _config) => {
|
||||
let songControls;
|
||||
let savedNotification;
|
||||
// TODO create banner function
|
||||
/** @param {Electron.BrowserWindow} win */
|
||||
module.exports = (win) => {
|
||||
songControls = getSongControls(win);
|
||||
config = _config;
|
||||
if (app.isPackaged && !config.smallInteractive) save_temp_icons();
|
||||
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||
|
||||
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||
|
||||
if (app.isPackaged) save_temp_icons();
|
||||
|
||||
let lastSongInfo = { url: undefined };
|
||||
|
||||
// Register songInfoCallback
|
||||
registerCallback(songInfo => {
|
||||
if (!songInfo.isPaused && (songInfo.url !== lastSongInfo.url || config.unpauseNotification)) {
|
||||
if (!songInfo.isPaused && (songInfo.url !== lastSongInfo.url || config.get("unpauseNotification"))) {
|
||||
lastSongInfo = { ...songInfo };
|
||||
sendXML(songInfo);
|
||||
}
|
||||
});
|
||||
|
||||
//TODO on app before close, close notification
|
||||
// TODO on app before close, close notification
|
||||
app.once("before-quit", () => {
|
||||
savedNotification?.close();
|
||||
});
|
||||
@ -34,9 +41,9 @@ module.exports = (win, _config) => {
|
||||
(cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd]();
|
||||
if (cmd === 'pause' || (cmd === 'play' && !config.unpauseNotification)) {
|
||||
if (cmd === 'pause' || (cmd === 'play' && !config.get("unpauseNotification"))) {
|
||||
setImmediate(() =>
|
||||
sendXML({ ...lastSongInfo, isPaused: cmd === 'pause' })
|
||||
sendXML({ ...lastSongInfo, isPaused: cmd === 'pause', elapsedSeconds: currentSeconds })
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -45,20 +52,20 @@ module.exports = (win, _config) => {
|
||||
}
|
||||
|
||||
function sendXML(songInfo) {
|
||||
const imgSrc = notificationImage(songInfo, true);
|
||||
const iconSrc = notificationImage(songInfo);
|
||||
|
||||
savedNotification?.close();
|
||||
|
||||
savedNotification = new Notification({
|
||||
title: songInfo.title || "Playing",
|
||||
body: songInfo.artist,
|
||||
icon: imgSrc,
|
||||
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_custom(),
|
||||
toastXml: get_xml(songInfo, iconSrc),
|
||||
});
|
||||
|
||||
savedNotification.on("close", (_) => {
|
||||
@ -68,209 +75,131 @@ function sendXML(songInfo) {
|
||||
savedNotification.show();
|
||||
}
|
||||
|
||||
const get_xml = (songInfo, iconSrc) => {
|
||||
switch (config.get("style")) {
|
||||
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("style") === ToastStyles.legacy ) {
|
||||
return `content="${icons[kind]}"`;
|
||||
} else {
|
||||
return `\
|
||||
content="${kind.charAt(0).toUpperCase() + kind.slice(1)}"\
|
||||
imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const getButton = (kind) =>
|
||||
const getButton = (kind) =>
|
||||
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||
|
||||
const display = (kind) =>
|
||||
config.smallInteractive ?
|
||||
`content="${icons[kind]}"` :
|
||||
`content="${kind.charAt(0).toUpperCase() + kind.slice(1)}" imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"`;
|
||||
|
||||
|
||||
const get_xml = (songInfo, options, imgSrc) => `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastImageAndText02">
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text id="1">${songInfo.title}</text>
|
||||
<text id="2">${songInfo.artist}</text>
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
const getButtons = (isPaused) => `\
|
||||
<actions>
|
||||
${getButton('previous')}
|
||||
${songInfo.isPaused ? getButton('play') : getButton('pause')}
|
||||
${isPaused ? getButton('play') : getButton('pause')}
|
||||
${getButton('next')}
|
||||
</actions>
|
||||
</toast>`
|
||||
</actions>\
|
||||
`;
|
||||
|
||||
// **************************************************** //
|
||||
// PREMADE TEMPLATES FOR TESTING
|
||||
// DELETE AFTER TESTING
|
||||
// **************************************************** //
|
||||
|
||||
const get_xml_custom = () => xml_banner_centered_top;
|
||||
|
||||
const xml_logo_ascii = `
|
||||
<toast useButtonStyles="true">
|
||||
const toast = (content, isPaused) => `\
|
||||
<toast>
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_cover.jpg" name="Image" placement="appLogoOverride"/>
|
||||
<text id="1">The Last Stand of Frej</text>
|
||||
<text id="2">Amon Amarth</text>
|
||||
${content}
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
<actions>
|
||||
<action content="ᐸ" activationType="protocol" arguments="youtubemusic://pause}"/>
|
||||
<action content="‖" activationType="protocol" arguments="youtubemusic://pause}"/>
|
||||
<action content="ᐳ" activationType="protocol" arguments="youtubemusic://pause}"/>
|
||||
</actions>
|
||||
</toast>
|
||||
`;
|
||||
${getButtons(isPaused)}
|
||||
</toast>`;
|
||||
|
||||
const xml_logo = ({title, artist, isPaused}, imgSrc) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" placement="appLogoOverride"/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused);
|
||||
|
||||
const xml_logo_icons_notext =`
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_cover.jpg" name="Image" placement="appLogoOverride"/>
|
||||
<text id="1">The Last Stand of Frej</text>
|
||||
<text id="2">Amon Amarth</text>
|
||||
</binding>
|
||||
</visual>
|
||||
const xml_hero = ({title, artist, isPaused}, imgSrc) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" placement="hero"/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused);
|
||||
|
||||
<actions>
|
||||
<action content=""
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/previous.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
<action content=""
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/pause.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
<action content=""
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/next.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
</actions>
|
||||
</toast>
|
||||
`;
|
||||
const xml_banner_bottom = ({title, artist, isPaused}, imgSrc) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused);
|
||||
|
||||
const buttons_icons = `
|
||||
<actions>
|
||||
<action content="Previous"
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/previous.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
<action content="Pause"
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/pause.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
<action content="Next"
|
||||
imageUri="file:///C:/Git/youtube-music/assets/media-icons-black/next.png"
|
||||
activationType="protocol" arguments="youtubemusic://pause}" />
|
||||
</actions>
|
||||
`;
|
||||
|
||||
const xml_logo_icons = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_cover.jpg" name="Image" placement="appLogoOverride"/>
|
||||
<text id="1">The Last Stand of Frej</text>
|
||||
<text id="2">Amon Amarth</text>
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
`;
|
||||
|
||||
const xml_hero = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_banner.jpg" name="Image" placement="hero"/>
|
||||
<text id="1">The Last Stand of Frej</text>
|
||||
<text id="2">Amon Amarth</text>
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
`;
|
||||
|
||||
const xml_banner_bottom = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_banner.jpg" name="Image" />
|
||||
<text id="1">The Last Stand of Frej</text>
|
||||
<text id="2">Amon Amarth</text>
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
`;
|
||||
|
||||
const xml_banner_top_custom = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_banner.jpg" name="Image" />
|
||||
const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup>
|
||||
<text hint-style="body">The Last Stand of Frej</text>
|
||||
<text hint-style="captionSubtle">Amon Amarth</text>
|
||||
<text hint-style="body">${songInfo.title}</text>
|
||||
<text hint-style="captionSubtle">${songInfo.artist}</text>
|
||||
</subgroup>
|
||||
<subgroup hint-textStacking="bottom">
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">Surtur Rising</text>
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">2011</text>
|
||||
</subgroup>
|
||||
</group>
|
||||
</binding>
|
||||
</visual>
|
||||
${xml_more_data(songInfo)}
|
||||
</group>\
|
||||
`, songInfo.isPaused);
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
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 = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
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="subHeader">The Last Stand of Frej</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">Amon Amarth</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="file:///C:/Git/test/toasters/assets/surtur_rising_banner.jpg" name="Image" hint-removeMargin="true" />
|
||||
</binding>
|
||||
</visual>
|
||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||
`, isPaused);
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
`;
|
||||
|
||||
const xml_banner_centered_top = `
|
||||
<toast useButtonStyles="true">
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<image id="1" src="file:///C:/Git/test/toasters/assets/surtur_rising_banner.jpg" name="Image" />
|
||||
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="subHeader">The Last Stand of Frej</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">Amon Amarth</text>
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>
|
||||
</binding>
|
||||
</visual>
|
||||
</group>\
|
||||
`, isPaused);
|
||||
|
||||
${buttons_icons}
|
||||
</toast>
|
||||
`;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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()
|
||||
@ -10,7 +11,7 @@ module.exports = (_win, options) => [
|
||||
label: level.name,
|
||||
type: "radio",
|
||||
checked: options.urgency === level.value,
|
||||
click: () => setOption(options, "urgency", level.value),
|
||||
click: () => config.set("urgency", level.value),
|
||||
})),
|
||||
},
|
||||
]
|
||||
@ -21,13 +22,12 @@ module.exports = (_win, options) => [
|
||||
label: "Interactive Notifications",
|
||||
type: "checkbox",
|
||||
checked: options.interactive,
|
||||
click: (item) => setOption(options, "interactive", item.checked),
|
||||
// doesn't update until restart
|
||||
click: (item) => config.setAndMaybeRestart("interactive", item.checked),
|
||||
},
|
||||
{
|
||||
label: "Smaller Interactive Notifications",
|
||||
type: "checkbox",
|
||||
checked: options.smallInteractive,
|
||||
click: (item) => setOption(options, "smallInteractive", item.checked),
|
||||
label: "Toast Style",
|
||||
submenu: getToastStyleMenuItems(options)
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@ -35,6 +35,29 @@ module.exports = (_win, 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 + 1);
|
||||
|
||||
arr[0] = {
|
||||
label: "Hide Button Text",
|
||||
type: "checkbox",
|
||||
checked: options.hideButtonText,
|
||||
click: (item) => config.set("hideButtonText", item.checked),
|
||||
}
|
||||
|
||||
// ToastStyles index starts from 1
|
||||
for (const [name, index] of Object.entries(ToastStyles)) {
|
||||
arr[index] = {
|
||||
label: snakeToCamel(name),
|
||||
type: "radio",
|
||||
checked: options.style === index,
|
||||
click: () => config.set("style", index),
|
||||
};
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
@ -1,11 +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 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}", // ᐅ
|
||||
@ -14,37 +25,47 @@ 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 = (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("style")) {
|
||||
case module.exports.ToastStyles.logo:
|
||||
case module.exports.ToastStyles.legacy:
|
||||
return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon);
|
||||
default:
|
||||
return this.saveImage(songInfo.image, tempBanner);
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return tempImage.crop({
|
||||
x: Math.round(margin / 2),
|
||||
y: 0,
|
||||
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`);
|
||||
@ -55,13 +76,16 @@ module.exports.save_temp_icons = () => {
|
||||
}
|
||||
};
|
||||
|
||||
function centerNativeImage(nativeImage) {
|
||||
const tempImage = nativeImage.resize({ height: 256 });
|
||||
const margin = Math.max((tempImage.getSize().width - 256), 0);
|
||||
|
||||
return tempImage.crop({
|
||||
x: Math.round(margin / 2),
|
||||
y: 0,
|
||||
width: 256, height: 256
|
||||
})
|
||||
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}`;
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ function setupMPRIS() {
|
||||
return player;
|
||||
}
|
||||
|
||||
/** @param {Electron.BrowserWindow} win */
|
||||
function registerMPRIS(win) {
|
||||
const songControls = getSongControls(win);
|
||||
const { playPause, next, previous, volumeMinus10, volumePlus10 } = songControls;
|
||||
@ -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;
|
||||
|
||||
@ -27,7 +27,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);
|
||||
|
||||
@ -95,6 +95,8 @@ function listenForApiLoad() {
|
||||
|
||||
function onApiLoaded() {
|
||||
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||
//setImmediate()
|
||||
ipcRenderer.send('apiLoaded');
|
||||
|
||||
// Remove upgrade button
|
||||
if (config.get("options.removeUpgradeButton")) {
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const config = require("../config");
|
||||
const is = require("electron-is");
|
||||
|
||||
module.exports.setupSongControls = () => {
|
||||
document.addEventListener('apiLoaded', e => {
|
||||
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
|
||||
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
|
||||
if (is.linux() && config.plugins.isEnabled('shortcuts')) { // MPRIS Enabled
|
||||
document.querySelector('video').addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
||||
}
|
||||
}, { once: true, passive: true })
|
||||
};
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const is = require('electron-is');
|
||||
const { getImage } = require("./song-info");
|
||||
|
||||
const config = require("../config");
|
||||
|
||||
global.songInfo = {};
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
@ -17,14 +14,63 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
||||
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||
const srcChangedEvent = new CustomEvent('srcChanged');
|
||||
|
||||
const singleton = (fn) => {
|
||||
let called = false;
|
||||
return (...args) => {
|
||||
if (called) return;
|
||||
called = true;
|
||||
return fn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.setupSeekedListener = singleton(() => {
|
||||
document.querySelector('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
||||
});
|
||||
|
||||
module.exports.setupTimeChangedListener = singleton(() => {
|
||||
const progressObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||
});
|
||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] });
|
||||
});
|
||||
|
||||
module.exports.setupRepeatChangedListener = singleton(() => {
|
||||
const repeatObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('repeatChanged', mutations[0].target.title);
|
||||
});
|
||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('repeatChanged', $('#right-controls .repeat').title);
|
||||
});
|
||||
|
||||
module.exports.setupVolumeChangedListener = singleton((api) => {
|
||||
$('video').addEventListener('volumechange', (_) => {
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
|
||||
module.exports = () => {
|
||||
document.addEventListener('apiLoaded', apiEvent => {
|
||||
if (config.plugins.isEnabled('tuna-obs') ||
|
||||
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
||||
setupTimeChangeListener();
|
||||
setupRepeatChangeListener();
|
||||
setupVolumeChangeListener(apiEvent.detail);
|
||||
}
|
||||
ipcRenderer.on("setupTimeChangedListener", async () => {
|
||||
this.setupTimeChangedListener();
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupRepeatChangedListener", async () => {
|
||||
this.setupRepeatChangedListener();
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupVolumeChangedListener", async () => {
|
||||
this.setupVolumeChangedListener(apiEvent.detail);
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupSeekedListener", async () => {
|
||||
this.setupSeekedListener();
|
||||
});
|
||||
|
||||
const video = $('video');
|
||||
// name = "dataloaded" and abit later "dataupdated"
|
||||
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
||||
@ -57,29 +103,3 @@ module.exports = () => {
|
||||
}
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
function setupTimeChangeListener() {
|
||||
const progressObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||
});
|
||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
|
||||
}
|
||||
|
||||
function setupRepeatChangeListener() {
|
||||
const repeatObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('repeatChanged', mutations[0].target.title);
|
||||
});
|
||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('repeatChanged', $('#right-controls .repeat').title);
|
||||
}
|
||||
|
||||
function setupVolumeChangeListener(api) {
|
||||
$('video').addEventListener('volumechange', (_) => {
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
}
|
||||
|
||||
@ -61,7 +61,8 @@ const handleData = async (responseText, win) => {
|
||||
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
||||
|
||||
const oldUrl = songInfo.imageSrc;
|
||||
songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0];
|
||||
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||
songInfo.imageSrc = thumbnails[thumbnails.length - 1]?.url.split("?")[0];
|
||||
if (oldUrl !== songInfo.imageSrc) {
|
||||
songInfo.image = await getImage(songInfo.imageSrc);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user