diff --git a/app/toastNotifier.js b/app/toastNotifier.js new file mode 100644 index 0000000..98cbb08 --- /dev/null +++ b/app/toastNotifier.js @@ -0,0 +1,22 @@ +const {getMainWindow} = require("./windowManager"); + +/** + * Sends a toast notification to the renderer UI when the main window is ready. + * + * @param {string} message + * @param {"success"|"error"|"warning"|"info"} [type="info"] + * @param {number} [duration=4000] + * @returns {boolean} Whether the toast was sent. + */ +function sendToast(message, type = "info", duration = 4000) { + const mainWindow = getMainWindow(); + + if (!mainWindow || mainWindow.isDestroyed() || mainWindow.webContents.isDestroyed()) { + return false; + } + + mainWindow.webContents.send("show-toast", {message, type, duration}); + return true; +} + +module.exports = {sendToast}; diff --git a/preload.js b/preload.js index a8233a1..58538a3 100644 --- a/preload.js +++ b/preload.js @@ -159,6 +159,18 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.on("update-downloaded", (_, info) => callback(info)), openReleasePage: () => ipcRenderer.send("open-release-page"), + + /** + * Subscribes to toast notifications emitted by the main process. + * + * @param {Function} callback - Function executed with toast data. + * @returns {Function} Removes the listener. + */ + onToast: (callback) => { + const listener = (_, toast) => callback(toast); + ipcRenderer.on("show-toast", listener); + return () => ipcRenderer.removeListener("show-toast", listener); + }, }); /** diff --git a/public/script/toast.js b/public/script/toast.js index d4b6406..5e8aa57 100644 --- a/public/script/toast.js +++ b/public/script/toast.js @@ -82,4 +82,13 @@ window.showWarning = (message, duration = 4000) => * Shortcut: info toast */ window.showInfo = (message, duration = 4000) => - window.showToast(message, "info", duration); \ No newline at end of file + window.showToast(message, "info", duration); + +/** + * Listens for toast notifications from the main process and displays them in the UI. + */ +window.electronAPI?.onToast(({message, type = "info", duration = 4000} = {}) => { + if (typeof message === "string") { + window.showToast(message, type, duration); + } +}); \ No newline at end of file diff --git a/server/helpers/getBrowser.helpers.js b/server/helpers/getBrowser.helpers.js index 95c197b..5edf7c7 100644 --- a/server/helpers/getBrowser.helpers.js +++ b/server/helpers/getBrowser.helpers.js @@ -2,6 +2,7 @@ const fs = require("fs"); const os = require("os"); const notify = require("./notify.helpers"); const { logger } = require("../logger"); +const {sendToast} = require("../../app/toastNotifier"); /** * Detects the first supported browser installed on the system. @@ -15,6 +16,7 @@ const { logger } = require("../logger"); * If no supported browser is found: * - A warning is logged * - A notification is displayed to the user + * - An in-app toast is displayed to the user * - "firefox" is returned as a fallback to allow yt-dlp to * handle the error gracefully without crashing the application * @@ -51,6 +53,11 @@ function getUserBrowser() { // No supported browser found => Notify User logger.warn("No supported browser found on the system"); notify.notifyFirefoxBrowserMissing(); + + sendToast( + "Firefox was not found on your system. Please install Firefox to import browser cookies.", + "error", + ); } else { logger.info("Browser found: firefox"); } diff --git a/test/unit/toastNotifier.test.js b/test/unit/toastNotifier.test.js new file mode 100644 index 0000000..fd553e3 --- /dev/null +++ b/test/unit/toastNotifier.test.js @@ -0,0 +1,38 @@ +const mockSend = jest.fn(); +const mockGetMainWindow = jest.fn(); + +jest.mock("../../app/windowManager", () => ({ + getMainWindow: mockGetMainWindow, +})); + +const {sendToast} = require("../../app/toastNotifier"); + +describe("toastNotifier", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("sends a toast to the active renderer", () => { + mockGetMainWindow.mockReturnValue({ + isDestroyed: () => false, + webContents: { + isDestroyed: () => false, + send: mockSend, + }, + }); + + expect(sendToast("Firefox is missing", "error", 5000)).toBe(true); + expect(mockSend).toHaveBeenCalledWith("show-toast", { + message: "Firefox is missing", + type: "error", + duration: 5000, + }); + }); + + test("does not throw when the renderer is unavailable", () => { + mockGetMainWindow.mockReturnValue(null); + + expect(sendToast("Firefox is missing", "error")).toBe(false); + expect(mockSend).not.toHaveBeenCalled(); + }); +});