diff --git a/README.md b/README.md index 8ded651..4d659f9 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ Freedom Loader can be configured either through the settings panel in the UI or ```json { "autoUpdate": true, + "systemTray": true, "discordRPC": true, "customTopBar": true, "autoCheckInfo": true, @@ -252,6 +253,7 @@ Freedom Loader can be configured either through the settings panel in the UI or | Option | Type | Default | Description | |------------------------------|---------|----------|------------------------------------------------------------| | `autoUpdate` | boolean | `true` | Enable automatic application updates | +| `systemTray` | boolean | `true` | Allow app to minimize on close | | `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration | | `customTopBar` | boolean | `true` | Use custom application top bar | | `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste | @@ -294,6 +296,7 @@ Freedom-Loader/ │ ├── pathValidator.js │ ├── splashManager.js │ ├── themeManager.js +│ ├── tray.js │ ├── windowManager.js │ └── ytDlpUpdater.js ├── build/ # Build resources and assets diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js index fb70db0..c83dd24 100644 --- a/app/ipcHandlers.js +++ b/app/ipcHandlers.js @@ -27,6 +27,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers"); */ const FEATURE_WHITELIST = new Set([ "autoUpdate", + "systemTray", "discordRPC", "customTopBar", "autoCheckInfo", diff --git a/app/tray.js b/app/tray.js new file mode 100644 index 0000000..08ccdd3 --- /dev/null +++ b/app/tray.js @@ -0,0 +1,100 @@ +const {app, Menu, Tray, nativeImage} = require("electron"); +const path = require("path"); +const {logger} = require("../server/logger"); // Ajuste le chemin si besoin +const fs = require("fs"); + +/** + * Global reference to the Tray instance to prevent garbage collection. + * @type {Tray | null} + */ +let tray = null; + +/** + * Creates and configures the cross-platform System Tray. + * + * Responsibilities: + * - Load the appropriate icon for the OS. + * - Build the right-click context menu. + * - Handle left-click behavior (toggle window visibility). + * + * @param {import('electron').BrowserWindow} mainWindow - The main application window. + * @returns {Tray} The created Tray instance. + */ +function createSystemTray(mainWindow) { + // Prevent creating multiple instances + if (tray) return tray; + + /** + * Resolve the icon path. + * Tip: Use a .png for Linux/macOS and a .ico for Windows for best results. + * Here we use a generic PNG assuming it exists in your resources folder. + */ + const iconPath = config.devMode ? + path.join(__dirname, "..", "build", "app-icon-64x64.png") : + path.join(process.resourcesPath, "build", "app-icon-64x64.png"); + + if (!fs.existsSync(iconPath)) { + logger.error(`❌ ERREUR : L'icône du Tray est introuvable à ce chemin : ${iconPath}`); + } else { + logger.info(`✅ Icône trouvée pour le Tray !`); + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon); + tray.setToolTip("Freedom Loader"); + + /** + * The context menu (Right-click). + */ + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show Freedom Loader", + click: () => { + mainWindow.show(); + }, + }, + {type: "separator"}, + { + label: "Quit", + click: () => { + /** + * Tell the app it's a deliberate exit, + * bypassing the "minimize to tray" behavior. + */ + app.isQuitting = true; + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + /** + * Left-click behavior: Toggle window visibility. + * Note: macOS doesn't usually use left-click on tray, but Windows/Linux do. + */ + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + mainWindow.show(); + mainWindow.focus(); + } + }); + + return tray; +} + +/** + * Safely destroys the tray icon to prevent OS-level crashes (especially on Linux/Wayland). + * Should be called right before the application quits. + */ +function destroyTray() { + if (tray && !tray.isDestroyed()) { + tray.destroy(); + tray = null; + logger.info("System Tray destroyed cleanly."); + } +} + +module.exports = {createSystemTray, destroyTray}; \ No newline at end of file diff --git a/app/windowManager.js b/app/windowManager.js index beedca5..5e7b1d8 100644 --- a/app/windowManager.js +++ b/app/windowManager.js @@ -81,6 +81,19 @@ async function createMainWindow() { throw err; } + /** + * Intercepts the window close event. + * If the app is not explicitly quitting (via Tray menu), + * cancels the destruction and hides the window instead. + */ + mainWindow.on('close', (event) => { + if (!app.isQuitting) { + event.preventDefault(); + mainWindow.hide(); + return false; + } + }); + /** * Cleans up window reference when the main window is closed. * Prevents memory leaks and allows recreation if needed. diff --git a/build/app-icon-64x64.png b/build/app-icon-64x64.png new file mode 100644 index 0000000..d2da758 Binary files /dev/null and b/build/app-icon-64x64.png differ diff --git a/config/config.default.json b/config/config.default.json index 946ddf7..529c61c 100644 --- a/config/config.default.json +++ b/config/config.default.json @@ -1,5 +1,6 @@ { "autoUpdate": true, + "systemTray": true, "discordRPC": true, "customTopBar": true, "autoCheckInfo": true, diff --git a/main.js b/main.js index 2ffb95f..d6990d1 100644 --- a/main.js +++ b/main.js @@ -80,7 +80,9 @@ const { createMainWindow, getMainWindow } = require("./app/windowManager"); const { registerIpcHandlers } = require("./app/ipcHandlers"); const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager"); const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers"); +const {createSystemTray, destroyTray} = require("./app/tray"); +app.isQuitting = false; /** * If another instance want to run @@ -127,6 +129,11 @@ app.whenReady().then(async () => { closeSplashWindow(); getMainWindow().show(); + + if (configFeatures.systemTray) { + createSystemTray(getMainWindow()); + } + if (configFeatures.discordRPC) startRPC(); if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow()); @@ -138,11 +145,17 @@ app.whenReady().then(async () => { }); app.on("window-all-closed", () => { - logger.info("Shutting down..."); - app.quit(); + if (app.isQuitting) { + logger.info("Shutting down..."); + app.quit(); + } else if (process.platform !== "darwin") { + logger.info("Main window closed, app running in background (Tray)."); + } }); app.on("before-quit", async () => { + app.isQuitting = true; + destroyTray(); await stopRPC(); logger.info("All services stopped. Have a nice day!"); logSessionEnd(); diff --git a/public/index.html b/public/index.html index 6d1f90a..efe4a0a 100644 --- a/public/index.html +++ b/public/index.html @@ -127,13 +127,21 @@ - + +