From 225ccf4d2dd8e2f4bae91be140ec445335a9e8e8 Mon Sep 17 00:00:00 2001 From: MasterAcnolo Date: Sun, 9 Aug 2026 09:58:55 +0200 Subject: [PATCH] feat: allow app to minimize on the systemTray. Could be disable on purpose. --- README.md | 3 ++ app/ipcHandlers.js | 1 + app/tray.js | 100 +++++++++++++++++++++++++++++++++++++ app/windowManager.js | 13 +++++ build/app-icon-64x64.png | Bin 0 -> 2071 bytes config/config.default.json | 1 + main.js | 17 ++++++- public/index.html | 20 +++++--- 8 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 app/tray.js create mode 100644 build/app-icon-64x64.png 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 0000000000000000000000000000000000000000..d2da758f8ab602d0915a549fde8ccb6994c168b5 GIT binary patch literal 2071 zcmZ{lc{J3G8pnUgGLtP#6lT<0whUu18oP;ClYLSXy%@%lR~pj{*@m%}wXv@;QCciR zNo6-Rnh-*^oNQyPB~fT|-E+^m=iGD8`^WSBp6B_V&-woOBs)3SNPrK60RWJ|p0>j6 zxy8RnY~P*&gy-k>RE2mNXAb}ossKPw0D$j%C;B1)gdhN5nE(Jr1ppu)T+-xhvKI(? z+uB$GyVq~_w(o7CRII(V=(3n7#1MX{Fp3WVB0sQJmM)h*+$wax;f{yAiV2*OB~tA5 zmVCM#gD)^dk{j(w%XFTT z6o+GqZ&>TW?NZ4MuR*&a%X`W=y{*#u7dy}VV{S#xnw3ztb|!Xrzq6i?M|~R&S>C>ar~r7LSGvjBJ!5~BCK_m;`&x(9tdiz<=)4+R7%Ueq|O&8Wzw>S zg%pp)xOWOm99UXP>027#6x39=Ma90Zp2#F|$%hn0w9sLl9uyg)60U)1h52*}rWUiG z5Hm7tiH1AcMk-JtZBERSINX<1!G`dJTJ1*<(;k%`7%MZ_RPg6nh>hKi7j{gHOH!d7 zmF=xl??H4M)nMgfrBlX~JkHirXd-5U)jHyhTR$G3;OoJ^`kU>Q z@u)Q_+PI&5QhpPu^H@o&Qz3DLYfFe(!4mpQw;8^b+dtW<}8Po#Hd+jLkuAU|2rd9$o_Q*H00#Q>PStf;sLMEhz=i2vc-P8KC4)wdiv z;zrh=SwHcCmN2>lf}uTeY!P$*S7g38spc3Ix;2}n@!2N2I~^=MBVs=1n5(v|K4vgS zc^I#A%)B3cC2yUaO)0r1Qlu)UBwY0JvhMog#^LtL>nfXJxuahu6`=AfvrHw4s=1iZ zyevj}xQo>Jcw{r_3m9}$Xko|qL6Y%^?^H%>xBDn!r$lCn?#V}t?!fVyDB1%LHF8@D z;tlq9;fbZLSWx7dL&O|7?$g|m1B{7-**Q)cr(9Ls%6`OAxHdmj*aaS|pmpMvj6q{p z%9{6wI}2kfJYfP8t6aR1D2)k*6qWiv$x&WaS*cWfTS{O{(pN64+*zHM9`_D((|%r= z85E4i#-57Glk_PL<%BTAj)Q?}1u-eFnj_AF5{+jaZYjlcABz4Vdy&5Elj2CzS7E8zJu z?$aPF=5wOt#p*hoYPss&u=CYc5as~!h*v43Rc)U+y#b$99P(nB=_1^yUX378u?raV zMBi?7qZH|#+RvTL;x7e5$4_;;wjM04agm1%$9@ZXI*OePsiX^zZfXTorPp>tq4lzv zwCe5;T4FD>gVjq0%l6lx1lI<9*}nxpV<+9DzfWg$lY(((apx7Mi3bb4hJVR{0;Ma; zJ0G7`Z|hH9ZSNWJYrN3XeN;+bZdURcM1J_>49(%QKi$BMarOe=9!e;eYzx;;doR_Z zOYh%B!_O5|^!tS3)^_>Y!5u7eN_OFPV5se!b&q=nIe~U;w`_m;FXk&SrDPrx+Qffq z=$eGajc}U!eMU zQGJbwSAF*kKq8PRIN~H6Vd#R?H8L { 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 @@ - + +