import { app, type BrowserWindow, Notification } from 'electron'; import playIcon from '@assets/media-icons-black/play.png?asset&asarUnpack'; import pauseIcon from '@assets/media-icons-black/pause.png?asset&asarUnpack'; import nextIcon from '@assets/media-icons-black/next.png?asset&asarUnpack'; import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpack'; import { notificationImage, secondsToMinutes, ToastStyles } from './utils'; import getSongControls from '@/providers/song-controls'; import registerCallback, { type SongInfo, SongInfoEvent, } from '@/providers/song-info'; import { changeProtocolHandler } from '@/providers/protocol-handler'; import { setTrayOnClick, setTrayOnDoubleClick } from '@/tray'; import { mediaIcons } from '@/types/media-icons'; import type { NotificationsPluginConfig } from './index'; import type { BackendContext } from '@/types/contexts'; let songControls: ReturnType; let savedNotification: Notification | undefined; type Accessor = () => T; export default ( win: BrowserWindow, config: Accessor, { ipc: { on, send } }: BackendContext, ) => { 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), }); // To fix the notification not closing setTimeout(() => savedNotification?.close(), 5000); 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) => ``; const getButtons = (isPaused: boolean) => `\ ${getButton('previous')} ${isPaused ? getButton('play') : getButton('pause')} ${getButton('next')} \ `; const toast = (content: string, isPaused: boolean) => `\ `; const xmlImage = ( { title, artist, isPaused }: SongInfo, imgSrc: string, placement: string, ) => toast( `\ ${title} ${artist}\ `, 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( `\ ${songInfo.title} ${songInfo.artist} ${xmlMoreData(songInfo)} \ `, songInfo.isPaused ?? false, ); const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\ ${ album ? `${album}` : '' } ${secondsToMinutes( elapsedSeconds ?? 0, )} / ${secondsToMinutes(songDuration)} \ `; const xmlBannerCenteredBottom = ( { title, artist, isPaused }: SongInfo, imgSrc: string, ) => toast( `\ ${title} ${artist} \ `, isPaused ?? false, ); const xmlBannerCenteredTop = ( { title, artist, isPaused }: SongInfo, imgSrc: string, ) => toast( `\ ${title} ${artist} \ `, 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; on('ytmd:player-api-loaded', () => send('ytmd:setup-time-changed-listener')); let savedSongInfo: SongInfo; let lastUrl: string | undefined; // Register songInfoCallback registerCallback((songInfo, event) => { if (event === SongInfoEvent.TimeChanged) { currentSeconds = songInfo.elapsedSeconds ?? 0; } if (!songInfo.artist && !songInfo.title) { return; } savedSongInfo = { ...songInfo }; if ( !songInfo.isPaused && (songInfo.url !== lastUrl || config().unpauseNotification) ) { lastUrl = songInfo.url; sendNotification(songInfo); } }); if (config().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, ...args) => { if (Object.keys(songControls).includes(cmd)) { // @ts-expect-error: cmd is a key of songControls songControls[cmd as keyof typeof songControls](...args); if ( config().refreshOnPlayPause && (cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification)) ) { setImmediate(() => sendNotification({ ...savedSongInfo, isPaused: cmd === 'pause', elapsedSeconds: currentSeconds, }), ); } } }); };