Compare commits

...

8 Commits

Author SHA1 Message Date
MasterAcnolo
2a9b4420de fix(tray): dynamically create/destroy tray icon on setting toggle
Previously, toggling the system tray in the settings updated the configuration but did not instantiate or destroy the tray immediately. This caused a desynchronization bug where the application could minimize to a non-existent tray, creating a ghost instance.

The `set-feature` IPC handler now intercepts the `systemTray` key to apply the state at runtime before persisting the config.
2026-08-14 22:11:56 +02:00
MasterAcnolo
e521747baa feat: custom signature in every file download, as a comment 2026-08-14 22:08:41 +02:00
MasterAcnolo
2d71449a10 fix: tray behaviror. App will shutdown gracefully 2026-08-14 22:08:23 +02:00
MasterAcnolo
0a152d4b10 fix: rename "listeners" in "progressListeners", that enhance readability 2026-08-14 22:05:12 +02:00
MasterAcnolo
676607b2c9 fix: disable systemTray by default. Could be reenable by default on a next release. 2026-08-14 21:39:13 +02:00
MasterAcnolo
def3ced215 update: package-lock.json 2026-08-14 17:22:31 +02:00
MasterAcnolo
febe8364cd fix: log session date format (W Sweden) 2026-08-14 17:22:07 +02:00
MasterAcnolo
25d3b6e8e7 fix: false warn about DownloadPath at startup 2026-08-14 17:18:28 +02:00
9 changed files with 88 additions and 41 deletions

View File

@@ -18,6 +18,7 @@ const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
const { userThemesPath } = require("../server/helpers/path.helpers");
const { createSystemTray, destroyTray } = require("./tray");
/**
* Security whitelist for feature flags that can be modified at runtime.
@@ -214,6 +215,21 @@ function registerIpcHandlers(getMainWindow) {
return true;
}
/**
* Dynamically intercepts and applies System Tray state changes at runtime.
* Instantiates or destroys the tray icon immediately to prevent window
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
*/
if (key === "systemTray") {
if (value === true) {
logger.info("System Tray enabled dynamically.");
createSystemTray(getMainWindow(), config.devMode);
} else {
logger.info("System Tray disabled dynamically.");
destroyTray();
}
}
configFeatures[key] = value;
fs.writeFileSync(

View File

@@ -56,28 +56,23 @@ function validateDownloadPath(userPath) {
if (!userPath) return getDefaultDownloadPath();
try {
const absolutePath = path.resolve(userPath);
/**
* Resolves filesystem symlinks to ensure canonical absolute path.
* Required to prevent path traversal or alias bypass.
*/
const resolved = fs.realpathSync(path.resolve(userPath));
if (!fs.existsSync(resolved)) {
fs.mkdirSync(resolved, { recursive: true });
logger.info(`Download folder created: ${resolved}`);
if (!fs.existsSync(absolutePath)) {
fs.mkdirSync(absolutePath, { recursive: true });
logger.info(`Download folder created: ${absolutePath}`);
}
const real = fs.realpathSync(resolved);
const real = fs.realpathSync(absolutePath);
if (!isSafePath(resolved)) {
if (!isSafePath(real)) {
throw new Error("Path not allowed: system folders are blocked!");
}
return real;
}
catch (err) {
catch (err) {
/**
* Handles invalid filesystem paths, permissions issues, or resolution failures.
* Logs diagnostic information before propagating a sanitized error.

View File

@@ -87,7 +87,7 @@ async function createMainWindow() {
* cancels the destruction and hides the window instead.
*/
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
if (!app.isQuitting && config.systemTray ) {
event.preventDefault();
mainWindow.hide();
return false;

View File

@@ -1,6 +1,6 @@
{
"autoUpdate": true,
"systemTray": true,
"systemTray": false,
"discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true,

31
main.js
View File

@@ -82,8 +82,20 @@ const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
const {createSystemTray, destroyTray} = require("./app/tray");
/**
* Global flag indicating if the application is intentionally shutting down.
* Used across the app to bypass the "minimize to tray on close" behavior.
* @type {boolean}
*/
app.isQuitting = false;
/**
* State flag to track the asynchronous cleanup process during shutdown.
* Prevents the application from exiting before services (RPC, Tray, Logs) are properly closed.
* @type {boolean}
*/
let isCleanedUp = false;
/**
* If another instance want to run
*/
@@ -145,7 +157,7 @@ app.whenReady().then(async () => {
});
app.on("window-all-closed", () => {
if (app.isQuitting) {
if (app.isQuitting || !configFeatures.systemTray) {
logger.info("Shutting down...");
app.quit();
} else if (process.platform !== "darwin") {
@@ -153,10 +165,25 @@ app.on("window-all-closed", () => {
}
});
app.on("before-quit", async () => {
app.on("before-quit", (event) => {
app.isQuitting = true;
if (!isCleanedUp) {
event.preventDefault();
(async () => {
try {
destroyTray();
await stopRPC();
} catch (err) {
logger.error("Error during cleanup:", err);
} finally {
logger.info("All services stopped. Have a nice day!");
logSessionEnd();
isCleanedUp = true;
app.quit();
}
})();
}
});

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "freedom-loader",
"version": "1.6.1",
"version": "1.6.2-preview",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freedom-loader",
"version": "1.6.1",
"version": "1.6.2-preview",
"license": "GPL-3.0-only",
"dependencies": {
"chalk": "^4.1.2",

View File

@@ -4,6 +4,7 @@ const getUserBrowser = require("./getBrowser.helpers.js");
const { ffmpegPath, denoPath} = require("./path.helpers.js");
const { configFeatures } = require("../../config.js");
const { logger } = require("../logger.js");
const config = require("../../config")
/**
* Validates whether the provided codec is supported by the application.
@@ -130,6 +131,14 @@ function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
outputTemplate = "%(playlist_index)02d - %(title)s.%(ext)s";
}
// Add a custom signature in each file downloaded with the app. It's a small things but i found it cool :)
if (configFeatures.addMetadata) {
args.push(
"--postprocessor-args",
`ffmpeg:-metadata comment="Downloaded with Freedom Loader v${config.version}"`
);
}
args.push("-o", path.join(outputFolder, outputTemplate));
args.push(url);

View File

@@ -78,7 +78,7 @@ const logger = createLogger({
* Start Log Session
*/
function logSessionStart(logDir, downloadPath) {
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
logger.info(`--- Starting session: ${new Date().toLocaleString('sv-SE')} ---`);
logger.info("============================================================")
logger.info(`Application Version: ${config.version}`)
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
@@ -88,7 +88,7 @@ function logSessionStart(logDir, downloadPath) {
* Stop Log Session
*/
function logSessionEnd() {
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
logger.info(`--- Ending session: ${new Date().toLocaleString('sv-SE')} ---`);
}
module.exports = {

View File

@@ -123,7 +123,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
* @param {string} [options.outputFolder] - Destination folder.
* @param {string} [options.playlistTitle] - Playlist title.
*
* @param {Function[]} listeners - Progress listeners.
* @param {Function[]} progressListeners - Progress listeners.
* @param {Function[]} speedListeners - Download speed listeners.
* @param {Function[]} stageListeners - Processing stage listeners.
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
@@ -132,7 +132,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
* Resolves with the final output folder path once the
* download has completed successfully.
*/
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
function fetchDownload(options, progressListeners, speedListeners, stageListeners, playlistInfoListeners) {
return new Promise((resolve, reject) => {
@@ -190,7 +190,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
child.on("close", code => {
currentDownloadProcess = null;
listeners.forEach(fn => fn("done"));
progressListeners.forEach(fn => fn("done"));
if (code === 0) resolve(safeOutputFolder);
else if (code === null) reject(new Error(`Download cancelled by user`));
else reject(new Error(`YT-DLP failed with code : ${code}`));
@@ -204,11 +204,11 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
// Progress Bar
if (line.startsWith("[download] Destination:")) {
listeners.forEach(fn => fn("reset"));
progressListeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading..."));
}
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
if (match) progressListeners.forEach(fn => fn(parseFloat(match[1])));
if (line.includes("MiB/s") || line.includes("KiB/s")) {
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);