feat: migration to TypeScript FINAL

Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2023-09-04 02:27:53 +09:00
parent c0d7972da3
commit 53f5bda382
72 changed files with 1290 additions and 693 deletions

View File

@ -1,49 +0,0 @@
const { Notification } = require('electron');
const is = require('electron-is');
const { notificationImage } = require('./utils');
const config = require('./config');
const registerCallback = require('../../providers/song-info');
const notify = (info) => {
// Fill the notification with content
const notification = {
title: info.title || 'Playing',
body: info.artist,
icon: notificationImage(info),
silent: true,
urgency: config.get('urgency'),
};
// Send the notification
const currentNotification = new Notification(notification);
currentNotification.show();
return currentNotification;
};
const setup = () => {
let oldNotification;
let currentUrl;
registerCallback((songInfo) => {
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);
}, 10);
}
});
};
/** @param {Electron.BrowserWindow} win */
module.exports = (win, options) => {
// Register the callback for new song information
is.windows() && options.interactive
? require('./interactive')(win)
: setup();
};

View File

@ -0,0 +1,51 @@
import { BrowserWindow, Notification } from 'electron';
import is from 'electron-is';
import { notificationImage } from './utils';
import config from './config';
import interactive from './interactive';
import registerCallback, { SongInfo } from '../../providers/song-info';
import type { ConfigType } from '../../config/dynamic';
type NotificationOptions = ConfigType<'notifications'>;
const notify = (info: SongInfo) => {
// Send the notification
const currentNotification = new Notification({
title: info.title || 'Playing',
body: info.artist,
icon: notificationImage(info),
silent: true,
urgency: config.get('urgency') as 'normal' | 'critical' | 'low',
});
currentNotification.show();
return currentNotification;
};
const setup = () => {
let oldNotification: Notification;
let currentUrl: string | undefined;
registerCallback((songInfo: SongInfo) => {
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);
}, 10);
}
});
};
export default (win: BrowserWindow, options: NotificationOptions) => {
// Register the callback for new song information
is.windows() && options.interactive
? interactive(win)
: setup();
};

View File

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

View File

@ -0,0 +1,5 @@
import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('notifications');
export default { ...config } as PluginConfig<'notifications'>;

View File

@ -1,38 +1,32 @@
const path = require('node:path');
import path from 'node:path';
const { Notification, app, ipcMain } = require('electron');
import { app, BrowserWindow, ipcMain, Notification } from 'electron';
const { notificationImage, icons, saveTempIcon, secondsToMinutes, ToastStyles } = require('./utils');
import { icons, notificationImage, saveTempIcon, secondsToMinutes, ToastStyles } from './utils';
import config from './config';
/**
* @type {PluginConfig}
*/
const config = require('./config');
import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info';
import { changeProtocolHandler } from '../../providers/protocol-handler';
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
const getSongControls = require('../../providers/song-controls');
const registerCallback = require('../../providers/song-info');
const { changeProtocolHandler } = require('../../providers/protocol-handler');
const { setTrayOnClick, setTrayOnDoubleClick } = require('../../tray');
let songControls: ReturnType<typeof getSongControls>;
let savedNotification: Notification | undefined;
let songControls;
let savedNotification;
/** @param {Electron.BrowserWindow} win */
module.exports = (win) => {
export default (win: BrowserWindow) => {
songControls = getSongControls(win);
let currentSeconds = 0;
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
if (app.isPackaged) {
saveTempIcon();
}
let savedSongInfo;
let lastUrl;
let savedSongInfo: SongInfo;
let lastUrl: string | undefined;
// Register songInfoCallback
registerCallback((songInfo) => {
@ -78,7 +72,7 @@ module.exports = (win) => {
changeProtocolHandler(
(cmd) => {
if (Object.keys(songControls).includes(cmd)) {
songControls[cmd]();
songControls[cmd as keyof typeof songControls]();
if (config.get('refreshOnPlayPause') && (
cmd === 'pause'
|| (cmd === 'play' && !config.get('unpauseNotification'))
@ -97,11 +91,18 @@ module.exports = (win) => {
);
};
function sendNotification(songInfo) {
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,
@ -111,7 +112,7 @@ function sendNotification(songInfo) {
// 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, iconSrc),
toastXml: getXml(songInfo, icon),
});
savedNotification.on('close', () => {
@ -121,7 +122,7 @@ function sendNotification(songInfo) {
savedNotification.show();
}
const getXml = (songInfo, iconSrc) => {
const getXml = (songInfo: SongInfo, iconSrc: string) => {
switch (config.get('toastStyle')) {
default:
case ToastStyles.logo:
@ -155,7 +156,7 @@ const iconLocation = app.isPackaged
? path.resolve(app.getPath('userData'), 'icons')
: path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
const display = (kind) => {
const display = (kind: keyof typeof icons) => {
if (config.get('toastStyle') === ToastStyles.legacy) {
return `content="${icons[kind]}"`;
}
@ -166,10 +167,10 @@ const display = (kind) => {
`;
};
const getButton = (kind) =>
const getButton = (kind: keyof typeof icons) =>
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
const getButtons = (isPaused) => `\
const getButtons = (isPaused: boolean) => `\
<actions>
${getButton('previous')}
${isPaused ? getButton('play') : getButton('pause')}
@ -177,7 +178,7 @@ const getButtons = (isPaused) => `\
</actions>\
`;
const toast = (content, isPaused) => `\
const toast = (content: string, isPaused: boolean) => `\
<toast>
<audio silent="true" />
<visual>
@ -189,19 +190,19 @@ const toast = (content, isPaused) => `\
${getButtons(isPaused)}
</toast>`;
const xmlImage = ({ title, artist, isPaused }, imgSrc, placement) => 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);
`, isPaused ?? false);
const xmlLogo = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
const xmlHero = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="hero"');
const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"');
const xmlBannerBottom = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, '');
const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, '');
const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\
const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => toast(`\
<image id="1" src="${imgSrc}" name="Image" />
<text></text>
<group>
@ -211,17 +212,17 @@ const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\
</subgroup>
${xmlMoreData(songInfo)}
</group>\
`, songInfo.isPaused);
`, songInfo.isPaused ?? false);
const xmlMoreData = ({ album, elapsedSeconds, songDuration }) => `\
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)} / ${secondsToMinutes(songDuration)}</text>
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text>
</subgroup>\
`;
const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
<text></text>
<group>
<subgroup hint-weight="1" hint-textStacking="center">
@ -230,9 +231,9 @@ const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`
</subgroup>
</group>
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
`, isPaused);
`, isPaused ?? false);
const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
<image id="1" src="${imgSrc}" name="Image" />
<text></text>
<group>
@ -241,9 +242,9 @@ const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
</subgroup>
</group>\
`, isPaused);
`, isPaused ?? false);
const titleFontPicker = (title) => {
const titleFontPicker = (title: string) => {
if (title.length <= 13) {
return 'Header';
}

View File

@ -1,9 +1,14 @@
const is = require('electron-is');
import is from 'electron-is';
const { urgencyLevels, ToastStyles, snakeToCamel } = require('./utils');
const config = require('./config');
import {BrowserWindow, MenuItem} from 'electron';
module.exports = (_win, options) => [
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
import config from './config';
import type { ConfigType } from '../../config/dynamic';
export default (_win: BrowserWindow, options: ConfigType<'notifications'>) => [
...(is.linux()
? [
{
@ -24,7 +29,7 @@ module.exports = (_win, options) => [
type: 'checkbox',
checked: options.interactive,
// Doesn't update until restart
click: (item) => config.setAndMaybeRestart('interactive', item.checked),
click: (item: MenuItem) => config.setAndMaybeRestart('interactive', item.checked),
},
{
// Submenu with settings for interactive notifications (name shouldn't be too long)
@ -34,19 +39,19 @@ module.exports = (_win, options) => [
label: 'Open/Close on tray click',
type: 'checkbox',
checked: options.trayControls,
click: (item) => config.set('trayControls', item.checked),
click: (item: MenuItem) => config.set('trayControls', item.checked),
},
{
label: 'Hide Button Text',
type: 'checkbox',
checked: options.hideButtonText,
click: (item) => config.set('hideButtonText', item.checked),
click: (item: MenuItem) => config.set('hideButtonText', item.checked),
},
{
label: 'Refresh on Play/Pause',
type: 'checkbox',
checked: options.refreshOnPlayPause,
click: (item) => config.set('refreshOnPlayPause', item.checked),
click: (item: MenuItem) => config.set('refreshOnPlayPause', item.checked),
},
],
},
@ -60,11 +65,11 @@ module.exports = (_win, options) => [
label: 'Show notification on unpause',
type: 'checkbox',
checked: options.unpauseNotification,
click: (item) => config.set('unpauseNotification', item.checked),
click: (item: MenuItem) => config.set('unpauseNotification', item.checked),
},
];
function getToastStyleMenuItems(options) {
export function getToastStyleMenuItems(options: ConfigType<'notifications'>) {
const array = Array.from({ length: Object.keys(ToastStyles).length });
// ToastStyles index starts from 1

View File

@ -1,19 +1,20 @@
const path = require('node:path');
import path from 'node:path';
import fs from 'node:fs';
const fs = require('node:fs');
import { app, NativeImage } from 'electron';
const { app } = require('electron');
import config from './config';
const config = require('./config');
import { cache } from '../../providers/decorators';
import { SongInfo } from '../../providers/song-info';
const icon = 'assets/youtube-music.png';
const userData = app.getPath('userData');
const temporaryIcon = path.join(userData, 'tempIcon.png');
const temporaryBanner = path.join(userData, 'tempBanner.png');
const { cache } = require('../../providers/decorators');
module.exports.ToastStyles = {
export const ToastStyles = {
logo: 1,
banner_centered_top: 2,
hero: 3,
@ -23,20 +24,20 @@ module.exports.ToastStyles = {
legacy: 7,
};
module.exports.icons = {
export const icons = {
play: '\u{1405}', // ᐅ
pause: '\u{2016}', // ‖
next: '\u{1433}', //
previous: '\u{1438}', //
};
module.exports.urgencyLevels = [
export const urgencyLevels = [
{ name: 'Low', value: 'low' },
{ name: 'Normal', value: 'normal' },
{ name: 'High', value: 'critical' },
];
const nativeImageToLogo = cache((nativeImage) => {
const nativeImageToLogo = cache((nativeImage: NativeImage) => {
const temporaryImage = nativeImage.resize({ height: 256 });
const margin = Math.max(temporaryImage.getSize().width - 256, 0);
@ -48,7 +49,7 @@ const nativeImageToLogo = cache((nativeImage) => {
});
});
module.exports.notificationImage = (songInfo) => {
export const notificationImage = (songInfo: SongInfo) => {
if (!songInfo.image) {
return icon;
}
@ -58,30 +59,30 @@ module.exports.notificationImage = (songInfo) => {
}
switch (config.get('toastStyle')) {
case module.exports.ToastStyles.logo:
case module.exports.ToastStyles.legacy: {
return this.saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
case ToastStyles.logo:
case ToastStyles.legacy: {
return saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
}
default: {
return this.saveImage(songInfo.image, temporaryBanner);
return saveImage(songInfo.image, temporaryBanner);
}
}
};
module.exports.saveImage = cache((img, savePath) => {
export const saveImage = cache((img: NativeImage, savePath: string) => {
try {
fs.writeFileSync(savePath, img.toPNG());
} catch (error) {
console.log(`Error writing song icon to disk:\n${error.toString()}`);
} catch (error: unknown) {
console.log(`Error writing song icon to disk:\n${String(error)}`);
return icon;
}
return savePath;
});
module.exports.saveTempIcon = () => {
for (const kind of Object.keys(module.exports.icons)) {
export const saveTempIcon = () => {
for (const kind of Object.keys(icons)) {
const destinationPath = path.join(userData, 'icons', `${kind}.png`);
if (fs.existsSync(destinationPath)) {
continue;
@ -94,13 +95,13 @@ module.exports.saveTempIcon = () => {
}
};
module.exports.snakeToCamel = (string_) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
export const snakeToCamel = (string_: string) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
group.toUpperCase()
.replace('-', ' ')
.replace('_', ' '),
);
module.exports.secondsToMinutes = (seconds) => {
export const secondsToMinutes = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const secondsLeft = seconds % 60;
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;