tiny-electron-essentials
Version:
A lightweight and modular utility library for Electron apps, offering simplified window management, tray support, IPC channels, and custom frameless window styling.
240 lines (207 loc) • 7.96 kB
JavaScript
;
var node_fs = require('node:fs');
var promises = require('node:fs/promises');
var node_path = require('node:path');
var electron = require('electron');
var Events = require('../global/Events.cjs');
var Utils = require('../global/Utils.cjs');
/**
* @typedef {Object} IconCacheData
* @property {string} tag - The unique tag identifying the notification.
* @property {string} iconFile - The filename of the cached icon.
* @property {boolean} isBase64 - Indicates if the icon was generated from a base64 string.
*/
/**
* @typedef {Object} NotificationResult
* @property {string|null} tag - The notification tag.
* @property {boolean} isSupported - Whether the system supports notifications.
*/
/**
* Provides an interface to manage notifications with event handling
* through Electron's native IPC. Each notification instance supports lifecycle
* management (create, show, close) and full event listener control.
*
* @beta This API is experimental and may change in future versions.
*/
class TinyElectronNotification {
#folderPath;
#Events = Events.NotificationEvents;
/** @type {Map<string, Electron.Notification>} */
#notifications = new Map();
/**
* Handles the IPC request to create a new notification.
*
* @param {Electron.IpcMainInvokeEvent} event - The IPC invoke event.
* @param {Electron.NotificationConstructorOptions & { tag?: string; }} data - Notification options.
* @returns {NotificationResult} The creation result.
*/
#handleCreate(event, data) {
const win = this.#getWin(event);
if (!win || typeof data !== 'object' || typeof data.tag !== 'string') {
return { tag: null, isSupported: false };
}
const { tag, ...notificationOptions } = data;
const iconCache = this.#processIcon(tag, notificationOptions.icon);
if (iconCache.isBase64) {
notificationOptions.icon = node_path.join(this.#folderPath, `./${iconCache.iconFile}`);
}
return this.#setupNotification(win, notificationOptions, iconCache);
}
/**
* Shows an existing notification.
*
* @param {string} tag - The notification identifier.
* @returns {null} Always returns null.
*/
#handleShow(tag) {
const noti = this.#notifications.get(tag);
if (noti) noti.show();
return null;
}
/**
* Closes an existing notification.
*
* @param {string} tag - The notification identifier.
* @returns {null} Always returns null.
*/
#handleClose(tag) {
const noti = this.#notifications.get(tag);
if (noti) noti.close();
return null;
}
/**
* Processes the notification icon, saving base64 data to a file if necessary.
*
* @param {string} tag - The notification tag.
* @param {string|Electron.NativeImage} [icon] - The icon data.
* @returns {IconCacheData} Information about the processed icon.
*/
#processIcon(tag, icon) {
const iconCache = { tag, iconFile: '', isBase64: false };
if (typeof icon === 'string' && icon.startsWith('data:image/')) {
const [header, base64Data] = icon.split(';base64,');
const ext = header.replace('data:image/', '');
const filename = `${tag.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.${ext}`;
const tempFile = node_path.join(this.#folderPath, `./${filename}`);
const binaryString = atob(base64Data);
node_fs.writeFileSync(tempFile, binaryString, 'binary');
iconCache.iconFile = filename;
iconCache.isBase64 = true;
}
return iconCache;
}
/**
* Creates the notification instance and sets up its event listeners.
*
* @param {BrowserWindow} win - The browser window to send IPC events to.
* @param {Electron.NotificationConstructorOptions} options - Electron notification options.
* @param {IconCacheData} iconCache - Information about the cached icon.
* @returns {NotificationResult} The initialization result.
*/
#setupNotification(win, options, iconCache) {
const { tag } = iconCache;
const noti = new electron.Notification(options);
this.#notifications.set(tag, noti);
// Clear
let isCleared = false;
const clearNotification = async () => {
if (isCleared) return;
isCleared = true;
try {
if (this.#notifications.has(tag) && iconCache.isBase64 && iconCache.iconFile) {
const filePath = node_path.join(this.#folderPath, `./${iconCache.iconFile}`);
if (node_fs.existsSync(filePath)) await promises.unlink(filePath);
}
} catch (err) {
console.error(err);
}
this.#notifications.delete(tag);
};
/**
* @param {string} eventName
* @param {string} eventType
* @param {Record<string|number|symbol, any>} extraData
*/
const sendEvent = (eventName, eventType, extraData = {}) => {
if (win && !win.isDestroyed() && win.webContents) {
const eventPayload = { tag, ...extraData };
win.webContents.send(eventName, { arg: eventPayload, time: Date.now() });
win.webContents.send(this.#Events.All, {
type: eventType,
arg: eventPayload,
time: Date.now(),
});
}
};
// Show
noti.on('show', () => sendEvent(this.#Events.Show, 'show'));
// Click
noti.on('click', () => {
sendEvent(this.#Events.Click, 'click');
clearNotification();
});
// Reply
noti.on('reply', (_e, reply) => {
sendEvent(this.#Events.Reply, 'reply', { reply });
clearNotification();
});
// Action
noti.on('action', (_e, index) => {
sendEvent(this.#Events.Action, 'action', { index });
clearNotification();
});
// Failed
noti.on('failed', (_e, error) => {
sendEvent(this.#Events.Failed, 'failed', { error });
clearNotification();
});
// Close
noti.on('close', () => {
sendEvent(this.#Events.Close, 'close');
clearNotification();
});
// Complete
return { tag, isSupported: electron.Notification.isSupported() };
}
/**
* Retrieves the corresponding BrowserWindow from an IPC event.
*
* @param {Electron.IpcMainInvokeEvent} event - The IPC event.
* @returns {BrowserWindow|null} The browser window, or null if not found.
*/
#getWin(event) {
const webContents = event.sender;
if (!event.senderFrame) return null;
return electron.BrowserWindow.fromWebContents(webContents) || null;
}
/**
* Deletes all files inside the configured folder path.
*
* @async
* @returns {Promise<void>} Resolves when all files are deleted.
*/
async deleteAllFilesInDir() {
const files = await promises.readdir(this.#folderPath);
const deleteTasks = files.map((file) => promises.unlink(node_path.join(this.#folderPath, file)));
await Promise.all(deleteTasks);
}
/**
* Initializes the notification manager and registers native IPC handlers.
*
* @param {Object} [settings={}] - Configuration settings for the notifications.
* @param {NotificationEvents} [settings.eventNames=this.#Events] - Set of event names for internal messaging.
* @param {string} [settings.folderPath] - Directory path to store temporary notification icons.
*/
constructor({ folderPath, eventNames = this.#Events } = {}) {
if (typeof folderPath !== 'string') throw new Error('folderPath must be a string.');
if (!node_fs.existsSync(folderPath) || !node_fs.lstatSync(folderPath).isDirectory())
throw new Error(`The folderPath "${folderPath}" does not exist or is not a directory.`);
this.#folderPath = folderPath;
Utils.checkEventsList(eventNames, this.#Events);
// Register native IPC handlers
electron.ipcMain.handle(this.#Events.Create, (event, data) => this.#handleCreate(event, data));
electron.ipcMain.handle(this.#Events.Show, (_event, tag) => this.#handleShow(tag));
electron.ipcMain.handle(this.#Events.Close, (_event, tag) => this.#handleClose(tag));
}
}
module.exports = TinyElectronNotification;