mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 11:01:45 +00:00
feat: migrate to new plugin api
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -1,5 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('notifications');
|
||||
|
||||
export default config;
|
||||
36
src/plugins/notifications/index.ts
Normal file
36
src/plugins/notifications/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
|
||||
export interface NotificationsPluginConfig {
|
||||
enabled: boolean;
|
||||
unpauseNotification: boolean;
|
||||
urgency: 'low' | 'normal' | 'critical';
|
||||
interactive: boolean;
|
||||
toastStyle: number;
|
||||
refreshOnPlayPause: boolean;
|
||||
trayControls: boolean;
|
||||
hideButtonText: boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('notifications', {
|
||||
name: 'Notifications',
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: 'normal', // Has effect only on Linux
|
||||
// the following has effect only on Windows
|
||||
interactive: true,
|
||||
toastStyle: 1, // See plugins/notifications/utils for more info
|
||||
refreshOnPlayPause: false,
|
||||
trayControls: true,
|
||||
hideButtonText: false,
|
||||
} as NotificationsPluginConfig,
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { app, BrowserWindow, ipcMain, Notification } from 'electron';
|
||||
import { app, BrowserWindow, Notification } from 'electron';
|
||||
|
||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||
import config from './config';
|
||||
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
@ -14,16 +13,209 @@ import pauseIcon from '../../../assets/media-icons-black/pause.png?asset&asarUnp
|
||||
import nextIcon from '../../../assets/media-icons-black/next.png?asset&asarUnpack';
|
||||
import previousIcon from '../../../assets/media-icons-black/previous.png?asset&asarUnpack';
|
||||
|
||||
import { MainPluginContext } from '../utils/builder';
|
||||
|
||||
import type { NotificationsPluginConfig } from './index';
|
||||
|
||||
let songControls: ReturnType<typeof getSongControls>;
|
||||
let savedNotification: Notification | undefined;
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
type Accessor<T> = () => T;
|
||||
|
||||
export default (
|
||||
win: BrowserWindow,
|
||||
config: Accessor<NotificationsPluginConfig>,
|
||||
{ on, send }: MainPluginContext<NotificationsPluginConfig>,
|
||||
) => {
|
||||
const sendNotification = (songInfo: SongInfo) => {
|
||||
const iconSrc = notificationImage(songInfo, config());
|
||||
|
||||
savedNotification?.close();
|
||||
|
||||
let icon: string;
|
||||
if (typeof iconSrc === 'object') {
|
||||
icon = iconSrc.toDataURL();
|
||||
} else {
|
||||
icon = iconSrc;
|
||||
}
|
||||
|
||||
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: getXml(songInfo, icon),
|
||||
});
|
||||
|
||||
savedNotification.on('close', () => {
|
||||
savedNotification = undefined;
|
||||
});
|
||||
|
||||
savedNotification.show();
|
||||
};
|
||||
|
||||
const getXml = (songInfo: SongInfo, iconSrc: string) => {
|
||||
switch (config().toastStyle) {
|
||||
default:
|
||||
case ToastStyles.logo:
|
||||
case ToastStyles.legacy: {
|
||||
return xmlLogo(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_top_custom: {
|
||||
return xmlBannerTopCustom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.hero: {
|
||||
return xmlHero(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_bottom: {
|
||||
return xmlBannerBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_bottom: {
|
||||
return xmlBannerCenteredBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_top: {
|
||||
return xmlBannerCenteredTop(songInfo, iconSrc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectIcon = (kind: keyof typeof mediaIcons): string => {
|
||||
switch (kind) {
|
||||
case 'play':
|
||||
return playIcon;
|
||||
case 'pause':
|
||||
return pauseIcon;
|
||||
case 'next':
|
||||
return nextIcon;
|
||||
case 'previous':
|
||||
return previousIcon;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const display = (kind: keyof typeof mediaIcons) => {
|
||||
if (config().toastStyle === ToastStyles.legacy) {
|
||||
return `content="${mediaIcons[kind]}"`;
|
||||
}
|
||||
|
||||
return `\
|
||||
content="${config().toastStyle ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
|
||||
imageUri="file:///${selectIcon(kind)}"
|
||||
`;
|
||||
};
|
||||
|
||||
const getButton = (kind: keyof typeof mediaIcons) =>
|
||||
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||
|
||||
const getButtons = (isPaused: boolean) => `\
|
||||
<actions>
|
||||
${getButton('previous')}
|
||||
${isPaused ? getButton('play') : getButton('pause')}
|
||||
${getButton('next')}
|
||||
</actions>\
|
||||
`;
|
||||
|
||||
const toast = (content: string, isPaused: boolean) => `\
|
||||
<toast>
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
${content}
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${getButtons(isPaused)}
|
||||
</toast>`;
|
||||
|
||||
const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
|
||||
|
||||
const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"');
|
||||
|
||||
const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, '');
|
||||
|
||||
const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => 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>
|
||||
${xmlMoreData(songInfo)}
|
||||
</group>\
|
||||
`, songInfo.isPaused ?? false);
|
||||
|
||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
|
||||
<subgroup hint-textStacking="bottom">
|
||||
${album
|
||||
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text>
|
||||
</subgroup>\
|
||||
`;
|
||||
|
||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>
|
||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const titleFontPicker = (title: string) => {
|
||||
if (title.length <= 13) {
|
||||
return 'Header';
|
||||
}
|
||||
|
||||
if (title.length <= 22) {
|
||||
return 'Subheader';
|
||||
}
|
||||
|
||||
if (title.length <= 26) {
|
||||
return 'Title';
|
||||
}
|
||||
|
||||
return 'Subtitle';
|
||||
};
|
||||
|
||||
|
||||
songControls = getSongControls(win);
|
||||
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||
on('apiLoaded', () => send('setupTimeChangedListener'));
|
||||
|
||||
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
|
||||
on('timeChanged', (t: number) => {
|
||||
currentSeconds = t;
|
||||
});
|
||||
|
||||
let savedSongInfo: SongInfo;
|
||||
let lastUrl: string | undefined;
|
||||
@ -36,14 +228,14 @@ export default (win: BrowserWindow) => {
|
||||
|
||||
savedSongInfo = { ...songInfo };
|
||||
if (!songInfo.isPaused
|
||||
&& (songInfo.url !== lastUrl || config.get('unpauseNotification'))
|
||||
&& (songInfo.url !== lastUrl || config().unpauseNotification)
|
||||
) {
|
||||
lastUrl = songInfo.url;
|
||||
sendNotification(songInfo);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.get('trayControls')) {
|
||||
if (config().trayControls) {
|
||||
setTrayOnClick(() => {
|
||||
if (savedNotification) {
|
||||
savedNotification.close();
|
||||
@ -73,9 +265,9 @@ export default (win: BrowserWindow) => {
|
||||
(cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd as keyof typeof songControls]();
|
||||
if (config.get('refreshOnPlayPause') && (
|
||||
if (config().refreshOnPlayPause && (
|
||||
cmd === 'pause'
|
||||
|| (cmd === 'play' && !config.get('unpauseNotification'))
|
||||
|| (cmd === 'play' && !config().unpauseNotification)
|
||||
)
|
||||
) {
|
||||
setImmediate(() =>
|
||||
@ -90,183 +282,3 @@ export default (win: BrowserWindow) => {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
function sendNotification(songInfo: SongInfo) {
|
||||
const iconSrc = notificationImage(songInfo);
|
||||
|
||||
savedNotification?.close();
|
||||
|
||||
let icon: string;
|
||||
if (typeof iconSrc === 'object') {
|
||||
icon = iconSrc.toDataURL();
|
||||
} else {
|
||||
icon = iconSrc;
|
||||
}
|
||||
|
||||
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: getXml(songInfo, icon),
|
||||
});
|
||||
|
||||
savedNotification.on('close', () => {
|
||||
savedNotification = undefined;
|
||||
});
|
||||
|
||||
savedNotification.show();
|
||||
}
|
||||
|
||||
const getXml = (songInfo: SongInfo, iconSrc: string) => {
|
||||
switch (config.get('toastStyle')) {
|
||||
default:
|
||||
case ToastStyles.logo:
|
||||
case ToastStyles.legacy: {
|
||||
return xmlLogo(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_top_custom: {
|
||||
return xmlBannerTopCustom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.hero: {
|
||||
return xmlHero(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_bottom: {
|
||||
return xmlBannerBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_bottom: {
|
||||
return xmlBannerCenteredBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_top: {
|
||||
return xmlBannerCenteredTop(songInfo, iconSrc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectIcon = (kind: keyof typeof mediaIcons): string => {
|
||||
switch (kind) {
|
||||
case 'play':
|
||||
return playIcon;
|
||||
case 'pause':
|
||||
return pauseIcon;
|
||||
case 'next':
|
||||
return nextIcon;
|
||||
case 'previous':
|
||||
return previousIcon;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const display = (kind: keyof typeof mediaIcons) => {
|
||||
if (config.get('toastStyle') === ToastStyles.legacy) {
|
||||
return `content="${mediaIcons[kind]}"`;
|
||||
}
|
||||
|
||||
return `\
|
||||
content="${config.get('hideButtonText') ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
|
||||
imageUri="file:///${selectIcon(kind)}"
|
||||
`;
|
||||
};
|
||||
|
||||
const getButton = (kind: keyof typeof mediaIcons) =>
|
||||
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||
|
||||
const getButtons = (isPaused: boolean) => `\
|
||||
<actions>
|
||||
${getButton('previous')}
|
||||
${isPaused ? getButton('play') : getButton('pause')}
|
||||
${getButton('next')}
|
||||
</actions>\
|
||||
`;
|
||||
|
||||
const toast = (content: string, isPaused: boolean) => `\
|
||||
<toast>
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
${content}
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${getButtons(isPaused)}
|
||||
</toast>`;
|
||||
|
||||
const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
|
||||
|
||||
const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"');
|
||||
|
||||
const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, '');
|
||||
|
||||
const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => 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>
|
||||
${xmlMoreData(songInfo)}
|
||||
</group>\
|
||||
`, songInfo.isPaused ?? false);
|
||||
|
||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
|
||||
<subgroup hint-textStacking="bottom">
|
||||
${album
|
||||
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text>
|
||||
</subgroup>\
|
||||
`;
|
||||
|
||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>
|
||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const titleFontPicker = (title: string) => {
|
||||
if (title.length <= 13) {
|
||||
return 'Header';
|
||||
}
|
||||
|
||||
if (title.length <= 22) {
|
||||
return 'Subheader';
|
||||
}
|
||||
|
||||
if (title.length <= 26) {
|
||||
return 'Title';
|
||||
}
|
||||
|
||||
return 'Subtitle';
|
||||
};
|
||||
|
||||
@ -1,25 +1,24 @@
|
||||
import { BrowserWindow, Notification } from 'electron';
|
||||
import { Notification } from 'electron';
|
||||
|
||||
import is from 'electron-is';
|
||||
|
||||
import { notificationImage } from './utils';
|
||||
import config from './config';
|
||||
import interactive from './interactive';
|
||||
|
||||
import builder, { NotificationsPluginConfig } from './index';
|
||||
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
type NotificationOptions = ConfigType<'notifications'>;
|
||||
let config: NotificationsPluginConfig = builder.config;
|
||||
|
||||
const notify = (info: SongInfo) => {
|
||||
// Send the notification
|
||||
const currentNotification = new Notification({
|
||||
title: info.title || 'Playing',
|
||||
body: info.artist,
|
||||
icon: notificationImage(info),
|
||||
icon: notificationImage(info, config),
|
||||
silent: true,
|
||||
urgency: config.get('urgency') as 'normal' | 'critical' | 'low',
|
||||
urgency: config.urgency,
|
||||
});
|
||||
currentNotification.show();
|
||||
|
||||
@ -31,7 +30,7 @@ const setup = () => {
|
||||
let currentUrl: string | undefined;
|
||||
|
||||
registerCallback((songInfo: SongInfo) => {
|
||||
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) {
|
||||
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.unpauseNotification)) {
|
||||
// Close the old notification
|
||||
oldNotification?.close();
|
||||
currentUrl = songInfo.url;
|
||||
@ -43,9 +42,17 @@ const setup = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export default (win: BrowserWindow, options: NotificationOptions) => {
|
||||
// Register the callback for new song information
|
||||
is.windows() && options.interactive
|
||||
? interactive(win)
|
||||
: setup();
|
||||
};
|
||||
export default builder.createMain((context) => {
|
||||
return {
|
||||
async onLoad(win) {
|
||||
config = await context.getConfig();
|
||||
|
||||
// Register the callback for new song information
|
||||
if (is.windows() && config.interactive) interactive(win, () => config, context);
|
||||
else setup();
|
||||
},
|
||||
onConfigChange(newConfig) {
|
||||
config = newConfig;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,93 +1,95 @@
|
||||
import is from 'electron-is';
|
||||
|
||||
import { BrowserWindow, MenuItem } from 'electron';
|
||||
import { MenuItem } from 'electron';
|
||||
|
||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||
|
||||
import config from './config';
|
||||
import builder, { NotificationsPluginConfig } from './index';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
import type { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
export default builder.createMenu(async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
const getMenu = (options: ConfigType<'notifications'>): MenuTemplate => {
|
||||
if (is.linux()) {
|
||||
return [
|
||||
{
|
||||
label: 'Notification Priority',
|
||||
submenu: urgencyLevels.map((level) => ({
|
||||
label: level.name,
|
||||
type: 'radio',
|
||||
checked: options.urgency === level.value,
|
||||
click: () => config.set('urgency', level.value),
|
||||
})),
|
||||
}
|
||||
];
|
||||
} else if (is.windows()) {
|
||||
return [
|
||||
{
|
||||
label: 'Interactive Notifications',
|
||||
type: 'checkbox',
|
||||
checked: options.interactive,
|
||||
// Doesn't update until restart
|
||||
click: (item: MenuItem) => 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: MenuItem) => config.set('trayControls', item.checked),
|
||||
},
|
||||
{
|
||||
label: 'Hide Button Text',
|
||||
type: 'checkbox',
|
||||
checked: options.hideButtonText,
|
||||
click: (item: MenuItem) => config.set('hideButtonText', item.checked),
|
||||
},
|
||||
{
|
||||
label: 'Refresh on Play/Pause',
|
||||
type: 'checkbox',
|
||||
checked: options.refreshOnPlayPause,
|
||||
click: (item: MenuItem) => config.set('refreshOnPlayPause', item.checked),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Style',
|
||||
submenu: getToastStyleMenuItems(options),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const getToastStyleMenuItems = (options: NotificationsPluginConfig) => {
|
||||
const array = Array.from({ length: Object.keys(ToastStyles).length });
|
||||
|
||||
export default (_win: BrowserWindow, options: ConfigType<'notifications'>): MenuTemplate => [
|
||||
...getMenu(options),
|
||||
{
|
||||
label: 'Show notification on unpause',
|
||||
type: 'checkbox',
|
||||
checked: options.unpauseNotification,
|
||||
click: (item: MenuItem) => config.set('unpauseNotification', item.checked),
|
||||
},
|
||||
];
|
||||
// ToastStyles index starts from 1
|
||||
for (const [name, index] of Object.entries(ToastStyles)) {
|
||||
array[index - 1] = {
|
||||
label: snakeToCamel(name),
|
||||
type: 'radio',
|
||||
checked: options.toastStyle === index,
|
||||
click: () => setConfig({ toastStyle: index }),
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
}
|
||||
|
||||
export function getToastStyleMenuItems(options: ConfigType<'notifications'>) {
|
||||
const array = Array.from({ length: Object.keys(ToastStyles).length });
|
||||
|
||||
// ToastStyles index starts from 1
|
||||
for (const [name, index] of Object.entries(ToastStyles)) {
|
||||
array[index - 1] = {
|
||||
label: snakeToCamel(name),
|
||||
type: 'radio',
|
||||
checked: options.toastStyle === index,
|
||||
click: () => config.set('toastStyle', index),
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
return array as Electron.MenuItemConstructorOptions[];
|
||||
}
|
||||
|
||||
return array as Electron.MenuItemConstructorOptions[];
|
||||
}
|
||||
const getMenu = (): MenuTemplate => {
|
||||
if (is.linux()) {
|
||||
return [
|
||||
{
|
||||
label: 'Notification Priority',
|
||||
submenu: urgencyLevels.map((level) => ({
|
||||
label: level.name,
|
||||
type: 'radio',
|
||||
checked: config.urgency === level.value,
|
||||
click: () => setConfig({ urgency: level.value }),
|
||||
})),
|
||||
}
|
||||
];
|
||||
} else if (is.windows()) {
|
||||
return [
|
||||
{
|
||||
label: 'Interactive Notifications',
|
||||
type: 'checkbox',
|
||||
checked: config.interactive,
|
||||
// Doesn't update until restart
|
||||
click: (item: MenuItem) => setConfig({ 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: config.trayControls,
|
||||
click: (item: MenuItem) => setConfig({ trayControls: item.checked }),
|
||||
},
|
||||
{
|
||||
label: 'Hide Button Text',
|
||||
type: 'checkbox',
|
||||
checked: config.hideButtonText,
|
||||
click: (item: MenuItem) => setConfig({ hideButtonText: item.checked }),
|
||||
},
|
||||
{
|
||||
label: 'Refresh on Play/Pause',
|
||||
type: 'checkbox',
|
||||
checked: config.refreshOnPlayPause,
|
||||
click: (item: MenuItem) => setConfig({ refreshOnPlayPause: item.checked }),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Style',
|
||||
submenu: getToastStyleMenuItems(config),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
...getMenu(),
|
||||
{
|
||||
label: 'Show notification on unpause',
|
||||
type: 'checkbox',
|
||||
checked: config.unpauseNotification,
|
||||
click: (item) => setConfig({ unpauseNotification: item.checked }),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@ -3,12 +3,11 @@ import fs from 'node:fs';
|
||||
|
||||
import { app, NativeImage } from 'electron';
|
||||
|
||||
import config from './config';
|
||||
|
||||
import { cache } from '../../providers/decorators';
|
||||
import { SongInfo } from '../../providers/song-info';
|
||||
|
||||
import youtubeMusicIcon from '../../../assets/youtube-music.png?asset&asarUnpack';
|
||||
import {NotificationsPluginConfig} from "./index";
|
||||
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
@ -27,9 +26,9 @@ export const ToastStyles = {
|
||||
};
|
||||
|
||||
export const urgencyLevels = [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Normal', value: 'normal' },
|
||||
{ name: 'High', value: 'critical' },
|
||||
{ name: 'Low', value: 'low' } as const,
|
||||
{ name: 'Normal', value: 'normal' } as const,
|
||||
{ name: 'High', value: 'critical' } as const,
|
||||
];
|
||||
|
||||
const nativeImageToLogo = cache((nativeImage: NativeImage) => {
|
||||
@ -44,16 +43,16 @@ const nativeImageToLogo = cache((nativeImage: NativeImage) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const notificationImage = (songInfo: SongInfo) => {
|
||||
export const notificationImage = (songInfo: SongInfo, config: NotificationsPluginConfig) => {
|
||||
if (!songInfo.image) {
|
||||
return youtubeMusicIcon;
|
||||
}
|
||||
|
||||
if (!config.get('interactive')) {
|
||||
if (!config.interactive) {
|
||||
return nativeImageToLogo(songInfo.image);
|
||||
}
|
||||
|
||||
switch (config.get('toastStyle')) {
|
||||
switch (config.toastStyle) {
|
||||
case ToastStyles.logo:
|
||||
case ToastStyles.legacy: {
|
||||
return saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
|
||||
|
||||
Reference in New Issue
Block a user