From 44132dd7b5cb314cac3db9ec93be4ec0d5594498 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Mon, 11 May 2026 21:14:00 +0200 Subject: [PATCH 01/11] fix: update dependencies --- package-lock.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed318fd..e60d081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -917,9 +917,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -2794,12 +2794,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -3549,9 +3549,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" From a6c11ba7c469ccdf7d4ff45c7ca1e3a407f3649c Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Fri, 15 May 2026 19:57:33 +0200 Subject: [PATCH 02/11] update: version for 1.6.0 --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 72c5381..0083bb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ### **A clean, open-source multimedia downloader for Windows** -[![Release](https://img.shields.io/badge/Release-1.5.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) +[![Release](https://img.shields.io/badge/Release-1.6.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/) [![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/) diff --git a/package-lock.json b/package-lock.json index e60d081..6ca13ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "freedom-loader", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freedom-loader", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "chalk": "^4.1.2", "debug": "^4.4.1", diff --git a/package.json b/package.json index 9fcef47..249ce17 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "freedom-loader", "productName": "Freedom Loader", - "version": "1.5.0", + "version": "1.6.0", "author": "MasterAcnolo", "description": "Freedom Loader", "main": "main.js", From 0f876fcb706f409186f53f92806e7ca7a80388f9 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Fri, 29 May 2026 07:41:54 +0200 Subject: [PATCH 03/11] fix: update dependencies --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ca13ab..4e54f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1355,9 +1355,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -4752,9 +4752,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5613,9 +5613,9 @@ } }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { From ac94d682241c2545a2ffba184fdeefd05466b56f Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Fri, 29 May 2026 07:42:38 +0200 Subject: [PATCH 04/11] fix: rename localMode => devMode for clarity --- app/windowManager.js | 2 +- main.js | 4 ++-- server/helpers/path.helpers.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/windowManager.js b/app/windowManager.js index 3b42bc7..5b35e89 100644 --- a/app/windowManager.js +++ b/app/windowManager.js @@ -12,7 +12,7 @@ async function createMainWindow() { return mainWindow; } - const iconPath = config.localMode + const iconPath = config.devMode ? path.join(__dirname, "../build/app-icon.ico") : path.join(process.resourcesPath, "build/app-icon.ico"); diff --git a/main.js b/main.js index 829a519..04ecc5f 100644 --- a/main.js +++ b/main.js @@ -30,7 +30,7 @@ app.setName("Freedom Loader"); app.setAppUserModelId("com.masteracnolo.freedomloader"); app.disableHardwareAcceleration(); -if (!config.localMode) { +if (!config.devMode) { const gotLock = app.requestSingleInstanceLock(); if (gotLock) { app.on("second-instance", () => { @@ -45,7 +45,7 @@ app.whenReady().then(async () => { createSplashWindow(); - if (!config.localMode && !checkNativeDependencies()) return; + if (!config.devMode && !checkNativeDependencies()) return; const { userYtDlp } = require("./server/helpers/path.helpers"); updateYtDlp(userYtDlp); diff --git a/server/helpers/path.helpers.js b/server/helpers/path.helpers.js index 38efa9e..b41cf8d 100644 --- a/server/helpers/path.helpers.js +++ b/server/helpers/path.helpers.js @@ -7,7 +7,7 @@ const config = require("../../config.js"); const { logger } = require("../logger.js"); // Centralized resource paths -const resourcesPath = config.localMode +const resourcesPath = config.devMode ? path.join(__dirname, "../../ressources") : process.resourcesPath; @@ -21,7 +21,7 @@ let denoPath; const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe"); -if (config.localMode) { +if (config.devMode) { userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe denoPath = path.join(__dirname, "../../ressources/deno.exe"); From 519dcad6e63a8a9dd3c45f4d111e04497a1bf9b1 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Fri, 29 May 2026 07:42:49 +0200 Subject: [PATCH 05/11] fix: rename localMode => devMode for clarity --- config.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index 3e8a43d..ff05cdc 100644 --- a/config.js +++ b/config.js @@ -3,10 +3,12 @@ const { app } = require("electron"); const fs = require("fs"); const path = require("path"); -const localMode = !app.isPackaged; + +const devMode = !app.isPackaged; + function resolveConfigPath() { - if (localMode) { + if (devMode) { const devConfigPath = path.join(__dirname, "config", "config.dev.json"); if (!fs.existsSync(devConfigPath)) { const defaultConfigPath = path.join(__dirname, "config", "config.default.json"); @@ -36,7 +38,7 @@ const configFeatures = loadFeatures(); module.exports = { version: packageJson.version, applicationPort: "8787", - localMode, + devMode, DiscordRPCID: "1410934537051181146", configFeatures, featuresPath From 6afc5e06d6f1378d57034a3b9ee8f4fe26b12812 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sun, 31 May 2026 15:48:20 +0200 Subject: [PATCH 06/11] fix: app startup. Can't start another instance now. --- main.js | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/main.js b/main.js index 04ecc5f..c5ebbc2 100644 --- a/main.js +++ b/main.js @@ -9,6 +9,23 @@ process.on("unhandledRejection", (reason) => { }); const { app } = require("electron"); + +/** + * True if this is the primary instance (lock acquired successfully) + */ +const isPrimaryInstance = app.requestSingleInstanceLock(); + +/** + * If we are on a second instance + */ +if (!isPrimaryInstance) { + app.quit(); + process.exit(0); +} + +/** + * Load the app + */ const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager"); const path = require("path"); @@ -30,15 +47,22 @@ app.setName("Freedom Loader"); app.setAppUserModelId("com.masteracnolo.freedomloader"); app.disableHardwareAcceleration(); -if (!config.devMode) { - const gotLock = app.requestSingleInstanceLock(); - if (gotLock) { - app.on("second-instance", () => { - logger.info("New instance detected, closing older..."); - getMainWindow()?.destroy(); - }); - } -} + +/** + * If another instance want to run + */ +app.on("second-instance", () => { + logger.info("Second instance detected"); + + const mainWindow = require("./app/windowManager").getMainWindow(); + + if (!mainWindow) return; + + if (mainWindow.isMinimized()) mainWindow.restore(); + + mainWindow.show(); + mainWindow.focus(); +}); app.whenReady().then(async () => { logSessionStart(); From 0218664c5a1a6e62baebcf6f5b60607267dcd3f9 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sun, 31 May 2026 20:38:42 +0200 Subject: [PATCH 07/11] refactor: add full documentation and fix theme loading robustness - Add JSDoc comments across frontend and backend modules - Improve code readability and maintainability with consistent documentation style - Document Electron IPC handlers, UI utilities, and theme system - Add missing function/class documentation in theme loader and app services - Minor structural improvements and cleanup across Electron main process modules - Minor fixes on variable names --- app/autoUpdater.js | 54 ++++++++ app/dependencyCheck.js | 31 +++++ app/discordRPC.js | 40 ++++++ app/ipcHandlers.js | 120 ++++++++++++++++-- app/pathValidator.js | 51 +++++++- app/splashManager.js | 53 +++++++- app/themeManager.js | 150 +++++++++++++++++++++++ app/windowManager.js | 50 ++++++++ app/ytDlpUpdater.js | 23 ++++ config.js | 76 +++++++++++- preload.js | 109 +++++++++++++++- public/script/appVersion.js | 23 +++- public/script/clipboardPaste.js | 73 ++++++----- public/script/custompath.js | 49 +++++++- public/script/customthemes.js | 130 ++++++++++++-------- public/script/downloadstatus.js | 47 +++++++ public/script/fetchinfo.js | 33 ++++- public/script/progressBar.js | 68 +++++++--- public/script/settingsPanel.js | 129 ++++++++++++------- public/script/toast.js | 81 ++++++++---- public/script/topbar.js | 58 ++++++--- server/controller/download.controller.js | 108 ++++++++++++++-- server/controller/info.controller.js | 144 ++++++++++++++-------- server/helpers/buildArgs.helpers.js | 42 +++++++ server/helpers/getBrowser.helpers.js | 33 ++++- server/helpers/notify.helpers.js | 48 +++++++- server/helpers/parseInfo.helpers.js | 53 ++++---- server/helpers/path.helpers.js | 49 ++++++-- server/helpers/rateLimit.helpers.js | 20 ++- server/helpers/validation.helpers.js | 40 ++++-- server/logger.js | 16 ++- server/server.js | 18 ++- server/services/download.services.js | 70 ++++++++++- server/services/info.services.js | 28 ++++- 34 files changed, 1777 insertions(+), 340 deletions(-) diff --git a/app/autoUpdater.js b/app/autoUpdater.js index 548020d..de06235 100644 --- a/app/autoUpdater.js +++ b/app/autoUpdater.js @@ -1,11 +1,42 @@ +/** + * Electron auto-update system using electron-updater. + * + * Handles: + * - update detection + * - user confirmation dialogs + * - download progress streaming to renderer + * - application restart and installation + */ + const { autoUpdater } = require("electron-updater"); const { dialog } = require("electron"); const { logger } = require("../server/logger"); +/** + * Configures autoUpdater behavior: + * - disables automatic download + * - disables automatic install on quit + * (manual user-controlled update flow) + */ autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; +/** + * Initializes application auto-update lifecycle. + * + * Registers update event listeners and binds them to: + * - UI dialogs (Electron dialog) + * - renderer communication (webContents) + * - download/install flow control + * + * @param {BrowserWindow} mainWindow - main Electron window instance + */ function initAutoUpdater(mainWindow) { + + /** + * Triggered when a new version is detected. + * Prompts user to install or defer update. + */ autoUpdater.on("update-available", async (info) => { logger.info(`Update available: ${info.version}`); @@ -28,6 +59,10 @@ function initAutoUpdater(mainWindow) { } }); + /** + * Streams update download progress to renderer process. + * Used to update UI progress bar and status indicators. + */ autoUpdater.on("download-progress", (progress) => { logger.info(`Download progress: ${Math.round(progress.percent)}%`); mainWindow?.webContents.send("update-progress", { @@ -36,6 +71,10 @@ function initAutoUpdater(mainWindow) { }); }); + /** + * Triggered when update has been fully downloaded. + * Prompts user to restart application and install update. + */ autoUpdater.on("update-downloaded", async (info) => { logger.info(`Update downloaded: ${info.version}`); @@ -52,6 +91,10 @@ function initAutoUpdater(mainWindow) { if (response === 0) autoUpdater.quitAndInstall(); }); + /** + * Handles update system errors. + * Logs failure and displays an error dialog to user. + */ autoUpdater.on("error", (err) => { logger.error("Auto update error:", err.message); dialog.showErrorBox("Update Error", err.message); @@ -60,6 +103,10 @@ function initAutoUpdater(mainWindow) { checkForUpdates(); } +/** + * Manually triggers update check on startup. + * Separated from init for reusability and testability. + */ async function checkForUpdates() { try { await autoUpdater.checkForUpdates(); @@ -68,6 +115,10 @@ async function checkForUpdates() { } } +/** + * Manually triggers update download. + * Used when user accepts update prompt. + */ async function downloadUpdate() { try { await autoUpdater.downloadUpdate(); @@ -76,6 +127,9 @@ async function downloadUpdate() { } } +/** + * Immediately quits application and installs downloaded update. + */ function installUpdate() { autoUpdater.quitAndInstall(); } diff --git a/app/dependencyCheck.js b/app/dependencyCheck.js index ba285c9..69501d2 100644 --- a/app/dependencyCheck.js +++ b/app/dependencyCheck.js @@ -2,9 +2,33 @@ const fs = require("fs"); const { app, dialog } = require("electron"); const { logger } = require("../server/logger"); +/** + * Verifies that all required native binaries exist before the app starts. + * + * This includes: + * - yt-dlp (download engine) + * - ffmpeg (media processing) + * - ffprobe (media inspection) + * - deno (JS runtime used by yt-dlp hooks) + * + * If any dependency is missing: + * - logs the error + * - shows a blocking Electron error dialog + * - exits the application + * + * @returns {boolean} true if all dependencies are present, false otherwise + */ function checkNativeDependencies() { + /** + * Native dependency paths are resolved at runtime from the server helpers. + * This ensures correct resolution in both dev and packaged builds. + */ const { binaryPaths } = require("../server/helpers/path.helpers"); + /** + * List of required external executables used by the application runtime. + * Each entry defines a binary name and its resolved absolute path. + */ const deps = [ { name: "yt-dlp.exe", path: binaryPaths.ytDlp }, { name: "ffmpeg.exe", path: binaryPaths.ffmpeg }, @@ -15,9 +39,16 @@ function checkNativeDependencies() { const missing = deps.filter(d => !fs.existsSync(d.path)); if (missing.length === 0) return true; + /** + * Human-readable list of missing binaries for logging and user dialog display. + */ const list = missing.map(d => d.name).join(", "); logger.error(`Missing dependencies: ${list}`); + /** + * Defers error dialog until Electron app is fully initialized. + * Required to safely display modal dialogs before quit. + */ app.whenReady().then(() => { dialog.showErrorBox( "Missing dependencies", diff --git a/app/discordRPC.js b/app/discordRPC.js index 49b5416..dcfbb4b 100644 --- a/app/discordRPC.js +++ b/app/discordRPC.js @@ -2,12 +2,40 @@ const config = require('../config'); const RPC = require("discord-rpc"); const { logger } = require("../server/logger"); +/** + * Discord Application Client ID used to authenticate the RPC connection. + * Comes from the application configuration. + */ const clientId = `${config.DiscordRPCID}`; + +/** + * Discord RPC client instance using IPC transport. + * Maintains a persistent connection with Discord desktop client. + */ const rpc = new RPC.Client({ transport: "ipc" }); +/** + * Optional interval reference used for future RPC refresh logic. + * Currently reserved for potential periodic activity updates. + */ let intervalId; +/** + * Initializes Discord Rich Presence (RPC) connection. + * + * Responsibilities: + * - Connect to Discord via IPC transport + * - Set application presence (title, state, timestamps, assets) + * - Handle connection errors gracefully via logger + * + * Triggered once during application startup. + */ function startRPC() { + + /** + * Rich Presence payload describing current application state + * shown in Discord user profile. + */ rpc.on("ready", () => { const presence = { largeImageKey: "icon", @@ -27,10 +55,22 @@ function startRPC() { }); } +/** + * Gracefully stops Discord Rich Presence connection. + * + * Responsibilities: + * - Clear any running intervals + * - Remove current activity from Discord + * - Destroy RPC transport connection + * - Handle cleanup errors safely + */ async function stopRPC(){ try { if (intervalId) clearInterval(intervalId); + /** + * Ensures RPC connection exists before attempting cleanup. + */ if (rpc && rpc.transport) { await rpc.clearActivity() await rpc.destroy(); diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js index 611dc91..dfa1114 100644 --- a/app/ipcHandlers.js +++ b/app/ipcHandlers.js @@ -1,3 +1,15 @@ +/** + * Electron IPC main process registry. + * + * Centralizes all IPC channels exposed to renderer process: + * - Application info (version, features) + * - Window controls (topbar actions) + * - File system interactions (theme, config, download paths) + * - UI state synchronization (progress bar) + * + * Acts as the bridge between renderer and privileged Node/Electron APIs. + */ + const { ipcMain, dialog, shell } = require("electron"); const fs = require("fs"); const path = require("path"); @@ -8,6 +20,12 @@ const config = require("../config"); const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator"); const { userThemesPath } = require("../server/helpers/path.helpers"); +/** + * Security whitelist for feature flags that can be modified at runtime. + * + * Prevents unauthorized or unexpected configuration keys from being persisted. + * Acts as a basic validation layer for IPC "set-feature". + */ const FEATURE_WHITELIST = new Set([ "autoUpdate", "discordRPC", @@ -23,17 +41,45 @@ const FEATURE_WHITELIST = new Set([ "notifySystem" ]); +/** + * Absolute path to the configuration file storing feature flags. + */ const configFolderPath = featuresPath; +/** + * Directory containing user-installed themes. + */ const themeFolderPath = userThemesPath; +/** + * Registers all IPC handlers and event listeners for Electron main process. + * + * This function wires renderer → main communication channels: + * - synchronous queries (handle) + * - fire-and-forget events (on) + * + * @param {Function} getMainWindow - Function returning current BrowserWindow instance + */ function registerIpcHandlers(getMainWindow) { - // Infos générales - ipcMain.handle("version", () => config.version); + /** + * Returns application version from config. + */ + ipcMain.handle("version", () => config.version); + + /** + * Returns runtime feature configuration object. + */ ipcMain.handle("features", () => configFeatures); - // Sélection et validation de dossier + /** + * Opens native folder selection dialog and validates selected path. + * + * Ensures: + * - user cancellation is handled safely + * - selected path is validated against security rules + * - unsafe directories are rejected + */ ipcMain.handle("select-download-folder", async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); if (result.canceled) { @@ -53,48 +99,108 @@ function registerIpcHandlers(getMainWindow) { ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath)); ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath()); - // Progression dans la taskbar + /** + * Updates Windows/macOS taskbar progress indicator. + * + * @param {number} percent - Progress value (0–100) + */ ipcMain.on("set-progress", (_, percent) => { getMainWindow()?.setProgressBar(percent / 100); }); - // TOPBAR ACTION + /** + * Window minimize request from renderer. + */ ipcMain.on("window-minimize", () => getMainWindow()?.minimize()); + + /** + * Toggles maximize/unmaximize state of main window. + */ ipcMain.on("window-maximize", () => { const win = getMainWindow(); if (!win) return; win.isMaximized() ? win.unmaximize() : win.maximize(); }); + + /** + * Closes the main application window. + */ ipcMain.on("window-close", () => getMainWindow()?.close()); + /** + * Opens Chromium DevTools in detached mode. + */ ipcMain.on("open-devtools", () => getMainWindow()?.webContents.openDevTools({ mode: "detach" }) ); + + /** + * Opens application logs directory in system file explorer. + */ ipcMain.on("open-logs", () => logDir && shell.openPath(logDir)); + + /** + * Opens external website in default browser. + */ ipcMain.on("open-website", () => shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/") ); + + /** + * Opens official wiki page in external browser. + */ ipcMain.on("open-wiki", () => shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki") ); + + /** + * Opens workshop page in external browser. + */ ipcMain.on("open-workshop", () => shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop") ); + + /** + * Opens configuration folder in system file explorer. + */ ipcMain.on("open-config", () => shell.openPath(configFolderPath)); - // THEME + /** + * Retrieves available themes from filesystem. + */ ipcMain.handle("get-themes", () => getThemes()); + /** + * Opens theme directory in file explorer. + */ ipcMain.on("open-theme", () => shell.openPath(themeFolderPath)); + /** + * Reloads themes from disk dynamically. + */ ipcMain.handle("reload-themes", async () => { return await reloadThemes(); }); - // Modification des features + /** + * Updates a runtime feature flag and persists it to disk. + * + * Flow: + * - Validates key against whitelist + * - Prevents unnecessary writes if value is unchanged + * - Updates in-memory config object + * - Persists to configuration file + * + * Security note: + * Only whitelisted keys can be modified via IPC to prevent config injection. + * + * @param {Electron.IpcMainEvent} event + * @param {{key: string, value: any}} payload + * @returns {boolean} success state + */ ipcMain.handle("set-feature", (event, { key, value }) => { try { if (!FEATURE_WHITELIST.has(key)) { diff --git a/app/pathValidator.js b/app/pathValidator.js index aa69107..94eb936 100644 --- a/app/pathValidator.js +++ b/app/pathValidator.js @@ -2,28 +2,77 @@ const fs = require("fs"); const path = require("path"); const { logger } = require("../server/logger"); +/** + * Cached resolved default download path. + * Initialized lazily on first access to avoid unnecessary imports. + */ let _defaultPath = null; +/** + * Returns the application's default download directory. + * + * This value is lazily loaded and cached after first call. + * The path originates from server configuration helpers. + * + * @returns {string} Absolute default download path + */ function getDefaultDownloadPath() { + if (!_defaultPath) { + + /** + * Lazy-loaded dependency providing the configured default download folder. + * Required only when function is executed to reduce startup cost. + */ const { defaultDownloadFolder } = require("../server/helpers/path.helpers"); + _defaultPath = defaultDownloadFolder; + } return _defaultPath; } +/** + * Validates and sanitizes a user-provided download path. + * + * Steps performed: + * - Resolves absolute real filesystem path + * - Normalizes symbolic links via realpathSync + * - Checks path safety rules (blocks system-critical directories) + * - Falls back to default path if input is empty + * + * @param {string} userPath - User-provided directory path + * @returns {string} Sanitized absolute path + * @throws Error If path is invalid, unsafe, or inaccessible + */ function validateDownloadPath(userPath) { + + /** + * Lazy-loaded helper used to enforce safe directory constraints. + * Prevents writes to protected system folders. + */ const { isSafePath } = require("../server/helpers/validation.helpers"); if (!userPath) return getDefaultDownloadPath(); try { + + /** + * Resolves filesystem symlinks to ensure canonical absolute path. + * Required to prevent path traversal or alias bypass. + */ const resolved = fs.realpathSync(path.resolve(userPath)); if (!isSafePath(resolved)) { throw new Error("Path not allowed: system folders are blocked!"); } return resolved; - } catch (err) { + } + catch (err) { + + /** + * Handles invalid filesystem paths, permissions issues, or resolution failures. + * Logs diagnostic information before propagating a sanitized error. + */ logger.error(`Invalid download path: ${userPath} — ${err.message}`); throw new Error(`Invalid or inaccessible path: ${err.message}`); } diff --git a/app/splashManager.js b/app/splashManager.js index acb5b37..f636497 100644 --- a/app/splashManager.js +++ b/app/splashManager.js @@ -1,11 +1,28 @@ const { BrowserWindow, app } = require("electron"); const path = require("path"); +/** + * Reference to the splash screen BrowserWindow instance. + * Used during application startup before main window is ready. + */ let splashWindow = null; +/** + * Creates and displays the splash screen window. + * + * Responsibilities: + * - Initializes a frameless, always-on-top splash window + * - Loads splash HTML UI + * - Dynamically injects banner image depending on dev/production mode + * - Acts as startup visual feedback before main window loads + */ function createSplashWindow() { -const splashOptions = { + /** + * Configuration for splash screen BrowserWindow. + * Optimized for minimal UI and fast startup display. + */ + const splashOptions = { width: 400, height: 300, frame: false, @@ -19,13 +36,29 @@ const splashOptions = { }, }; + /** + * Instantiates the splash screen window. + */ splashWindow = new BrowserWindow(splashOptions); + /** + * Absolute path to splash HTML file used as initial UI. + * Loaded locally from application bundle or project folder. + */ const splashPath = path.join(__dirname, "../public/splash.html"); splashWindow.loadFile(splashPath); - // Inject banner path for both dev and packaged app + /** + * Executes once splash HTML is fully loaded. + * Used here to inject dynamic assets (banner image path). + */ splashWindow.webContents.on('did-finish-load', () => { + + /** + * Resolves splash banner image path depending on runtime mode: + * - Production: bundled resources directory + * - Development: local build directory + */ let bannerPath; // Check if app is packaged @@ -36,12 +69,21 @@ const splashOptions = { bannerPath = path.join(__dirname, '../build/banner.png'); } + /** + * Injects runtime image source into splash DOM. + * Uses file:// protocol to load local image safely in Electron context. + */ splashWindow.webContents.executeJavaScript(` document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}'; `); }); } +/** + * Closes and cleans up the splash screen window. + * + * Should be called once main window is ready to display. + */ function closeSplashWindow() { if (splashWindow) { splashWindow.close(); @@ -49,6 +91,13 @@ function closeSplashWindow() { } } +/** + * Updates splash screen progress indicator from main process. + * + * Sends a step value to renderer via injected JS function. + * + * @param {number} step - Current initialization step or progress value + */ function setSplashProgress(step) { if (splashWindow) { splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`); diff --git a/app/themeManager.js b/app/themeManager.js index 9a997ee..b2e6616 100644 --- a/app/themeManager.js +++ b/app/themeManager.js @@ -1,14 +1,48 @@ +/** + * Theme loading and parsing system. + * + * Supports: + * - Folder-based themes + * - ZIP-based themes + * - Theme validation against required schema + * - Image embedding as base64 data URIs + * - Cached theme registry with sorted priority order + * + * Acts as the core theme ingestion pipeline for the application UI. + */ + const fs = require("fs"); const path = require("path"); const { logger } = require("../server/logger"); const JSZip = require("jszip"); +/** + * Maximum allowed size (in bytes) for theme images. + * Prevents memory abuse from large embedded assets. + */ const MAX_IMAGE_SIZE = 10 * 1024 * 1024; + +/** + * List of supported image file extensions for theme assets. + */ const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"]; + +/** + * Allowed base filenames for theme background/cover images. + * Restricts theme asset resolution to known conventions. + */ const ALLOWED_IMAGE_NAMES = ["cover", "background"]; +/** + * Priority order used to sort themes in UI. + * Themes not listed are placed at the end. + */ const THEME_ORDER = ["dark", "light"]; +/** + * Required JSON schema paths for a valid theme definition. + * Each entry represents a nested property path that must exist. + */ const REQUIRED_KEYS = [ ["meta", "name"], ["meta", "author"], @@ -20,10 +54,25 @@ const REQUIRED_KEYS = [ ["style", "progressBar", "fill"], ]; +/** + * Safely retrieves a deeply nested value from an object using a key path array. + * + * @param {object} obj - Target object + * @param {string[]} keys - Property path segments + * @returns {*} value or undefined if path is invalid + */ function getNestedValue(obj, keys) { return keys.reduce((acc, key) => acc?.[key], obj); } +/** + * Validates theme JSON structure against required schema. + * + * Ensures all mandatory configuration keys exist before theme is accepted. + * + * @param {object} json - Parsed theme JSON + * @returns {{valid: boolean, reason?: string}} + */ function validateThemeJson(json) { for (const keyPath of REQUIRED_KEYS) { if (getNestedValue(json, keyPath) === undefined) { @@ -33,6 +82,14 @@ function validateThemeJson(json) { return { valid: true }; } +/** + * Normalizes raw theme JSON into application-ready theme object. + * + * @param {string} themeId + * @param {object} themeJson + * @param {string|null} imageData - Base64 encoded theme image + * @returns {object} normalized theme object + */ function buildThemeObject(themeId, themeJson, imageData) { return { id: themeId.toLowerCase(), @@ -45,6 +102,17 @@ function buildThemeObject(themeId, themeJson, imageData) { }; } +/** + * Extracts and encodes an image from theme archive into base64 data URI. + * + * Applies safety checks: + * - Rejects images exceeding MAX_IMAGE_SIZE + * - Converts file extension into valid MIME type + * + * @param {Buffer} buffer + * @param {string} filename + * @returns {string|null} data URI or null if invalid + */ function extractImage(buffer, filename) { if (buffer.length > MAX_IMAGE_SIZE) { logger.warn(`Image too large, ignoring`); @@ -55,6 +123,20 @@ function extractImage(buffer, filename) { return `data:image/${mime};base64,${buffer.toString("base64")}`; } +/** + * Loads a theme from a ZIP archive. + * + * Steps: + * - Extract ZIP content + * - Locate .theme.json file + * - Validate JSON structure + * - Extract optional image asset + * - Build normalized theme object + * + * @param {string} zipPath + * @param {string} themeId + * @returns {Promise} + */ async function loadThemeFromZip(zipPath, themeId) { try { const zipBuffer = fs.readFileSync(zipPath); @@ -91,6 +173,15 @@ async function loadThemeFromZip(zipPath, themeId) { } } +/** + * Loads a theme from a directory structure. + * + * Equivalent to ZIP loader but works on filesystem directly. + * + * @param {string} folderPath + * @param {string} themeId + * @returns {Promise} + */ async function loadThemeFromFolder(folderPath, themeId) { try { const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json")); @@ -124,13 +215,38 @@ async function loadThemeFromFolder(folderPath, themeId) { } } +/** + * In-memory cache of loaded themes. + * Avoids re-parsing filesystem on every request. + */ let cachedThemes = null; + +/** + * Active theme directory path used as source of truth. + */ let themeFolderPath = null; +/** + * Sets the active theme directory path. + * + * @param {string} folderPath + */ function setThemeFolderPath(folderPath) { themeFolderPath = folderPath; } +/** + * Loads all themes from the configured theme directory. + * + * Supports: + * - ZIP themes (.zip) + * - Folder themes (directory-based) + * + * Iterates through filesystem entries and delegates parsing + * to the appropriate loader (ZIP or folder). + * + * @returns {Promise} List of valid theme objects + */ async function loadThemesFromFolder() { const themes = []; @@ -161,11 +277,31 @@ async function loadThemesFromFolder() { return themes; } +/** + * Returns sorting priority for a theme ID. + * + * Lower index = higher priority. + * Themes not in THEME_ORDER are placed at the end. + * + * @param {string} id + * @returns {number} + */ function getThemeOrder(id) { const index = THEME_ORDER.indexOf(id); return index === -1 ? Infinity : index; } +/** + * Initializes the theme system. + * + * Steps: + * - Sets theme folder path + * - Loads all themes from disk + * - Sorts themes by priority order + * - Caches result in memory + * + * @param {string} folderPath + */ async function initThemes(folderPath) { setThemeFolderPath(folderPath); const themes = await loadThemesFromFolder(); @@ -174,10 +310,24 @@ async function initThemes(folderPath) { logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`); } +/** + * Returns cached themes. + * + * If themes are not loaded yet, returns empty array. + * + * @returns {object[]} + */ function getThemes() { return cachedThemes ?? []; } +/** + * Reloads themes from disk and refreshes cache. + * + * Used when themes are modified at runtime without restart. + * + * @returns {Promise} + */ async function reloadThemes() { const themes = await loadThemesFromFolder(); cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id)); diff --git a/app/windowManager.js b/app/windowManager.js index 5b35e89..beedca5 100644 --- a/app/windowManager.js +++ b/app/windowManager.js @@ -1,21 +1,55 @@ +/** + * Electron main window module dependencies. + * Handles BrowserWindow lifecycle and application UI shell. + */ + const { BrowserWindow, app } = require("electron"); const path = require("path"); const { logger } = require("../server/logger"); const { configFeatures } = require("../config"); const config = require("../config"); +/** + * Singleton reference to the main Electron BrowserWindow instance. + * Ensures only one application window exists at runtime. + */ let mainWindow = null; +/** + * Creates and initializes the main Electron application window. + * + * Responsibilities: + * - Ensures singleton window instance (prevents duplicates) + * - Configures window options (size, icon, preload, security settings) + * - Loads frontend application from local dev server + * - Handles lifecycle events (close, errors) + * + * @returns {Promise} The created or existing window instance + */ async function createMainWindow() { + + /** + * Prevents multiple instances of the main window. + * Returns existing instance if already created. + */ if (mainWindow) { logger.warn("Window already exists, no new creation!"); return mainWindow; } + /** + * Resolves application icon path depending on runtime mode: + * - Development: local build folder + * - Production: packaged Electron resources + */ const iconPath = config.devMode ? path.join(__dirname, "../build/app-icon.ico") : path.join(process.resourcesPath, "build/app-icon.ico"); + /** + * Electron BrowserWindow configuration object. + * Defines UI behavior, security settings, and preload bridge. + */ const windowOptions = { title: `Freedom Loader ${config.version}`, icon: iconPath, @@ -35,6 +69,10 @@ async function createMainWindow() { mainWindow = new BrowserWindow(windowOptions); + /** + * Loads the frontend application served by the local dev server. + * In production, this could be replaced with a file:// build. + */ try { await mainWindow.loadURL(`http://localhost:${config.applicationPort}`); logger.info("Window loaded"); @@ -43,6 +81,10 @@ async function createMainWindow() { throw err; } + /** + * Cleans up window reference when the main window is closed. + * Prevents memory leaks and allows recreation if needed. + */ mainWindow.on("closed", () => { logger.info("Main window closed"); mainWindow = null; @@ -51,6 +93,14 @@ async function createMainWindow() { return mainWindow; } +/** + * Returns the current main window instance. + * + * Useful for IPC handlers and background services + * needing access to the renderer process. + * + * @returns {BrowserWindow | null} + */ function getMainWindow() { return mainWindow; } diff --git a/app/ytDlpUpdater.js b/app/ytDlpUpdater.js index 90b400b..0cfe8f5 100644 --- a/app/ytDlpUpdater.js +++ b/app/ytDlpUpdater.js @@ -1,10 +1,33 @@ const { execFile } = require("child_process"); const { logger } = require("../server/logger"); +/** + * Attempts to update the local yt-dlp binary using its built-in update command. + * + * This function: + * - Executes yt-dlp with the `-U` flag + * - Logs update output or warnings if update fails + * - Never throws: update failure is non-blocking by design + * + * @param {string} ytDlpPath - Absolute path to the yt-dlp executable + */ function updateYtDlp(ytDlpPath) { + logger.info("yt-dlp update check starting..."); + + /** + * Executes yt-dlp CLI process in a non-interactive mode. + * Used here specifically for self-update via `-U` flag. + */ execFile(ytDlpPath, ["-U"], (err, stdout) => { + + /** + * Handles yt-dlp update result: + * - Logs warning if update fails (network, binary issues, permissions) + * - Logs stdout output when update succeeds + */ if (err) logger.warn("yt-dlp update failed (continuing):", err.message); + else logger.info(`yt-dlp update: ${stdout.trim()}`); }); } diff --git a/config.js b/config.js index ff05cdc..73e855a 100644 --- a/config.js +++ b/config.js @@ -3,43 +3,107 @@ const { app } = require("electron"); const fs = require("fs"); const path = require("path"); - +/** + * Indicates whether the application is running in development mode. + * Determined by Electron's packaging state. + */ const devMode = !app.isPackaged; - +/** + * Resolves the configuration file path depending on runtime environment. + * + * In development: + * - Uses local config/config.dev.json + * - Creates it from default if missing + * + * In production: + * - Uses app userData/config.json + * - Creates it from bundled default config if missing + * + * @returns {string} Absolute path to the active configuration file + */ function resolveConfigPath() { if (devMode) { const devConfigPath = path.join(__dirname, "config", "config.dev.json"); + if (!fs.existsSync(devConfigPath)) { - const defaultConfigPath = path.join(__dirname, "config", "config.default.json"); + const defaultConfigPath = path.join( + __dirname, + "config", + "config.default.json" + ); fs.copyFileSync(defaultConfigPath, devConfigPath); } + return devConfigPath; } const userConfigPath = path.join(app.getPath("userData"), "config.json"); + if (!fs.existsSync(userConfigPath)) { - const defaultConfigPath = path.join(process.resourcesPath, "config", "config.default.json"); + const defaultConfigPath = path.join( + process.resourcesPath, + "config", + "config.default.json" + ); fs.copyFileSync(defaultConfigPath, userConfigPath); } return userConfigPath; } +/** + * Path to the active configuration file used at runtime. + */ const featuresPath = resolveConfigPath(); +/** + * Loads and parses the feature configuration file. + * + * @returns {Object} Parsed feature flags configuration + * @throws {Error} If the configuration file is missing or invalid + */ function loadFeatures() { const raw = fs.readFileSync(featuresPath, "utf-8"); return JSON.parse(raw); } +/** + * In-memory snapshot of application feature flags. + * + * Note: Changes to the config file are not automatically reflected. + * A restart or reload is required. + */ const configFeatures = loadFeatures(); module.exports = { + /** + * Application version from package.json + */ version: packageJson.version, + + /** + * Backend Express / internal server port + */ applicationPort: "8787", + + /** + * Indicates whether the app is running in development mode + */ devMode, + + /** + * Discord RPC application identifier + */ DiscordRPCID: "1410934537051181146", + + /** + * Runtime feature flags loaded from configuration file + */ configFeatures, - featuresPath -} \ No newline at end of file + + /** + * Path to the active configuration file + */ + featuresPath, +}; \ No newline at end of file diff --git a/preload.js b/preload.js index a136cc5..f280b7b 100644 --- a/preload.js +++ b/preload.js @@ -1,27 +1,124 @@ const { contextBridge, ipcRenderer } = require("electron"); +/** + * Exposes the main application API to the renderer process. + * + * This API acts as a secure bridge between the renderer and the + * Electron main process using IPC (Inter-Process Communication). + * All calls are explicitly whitelisted. + */ contextBridge.exposeInMainWorld("electronAPI", { + + /** + * Returns the default system download directory. + */ getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"), + + /** + * Opens a native dialog allowing the user to select a download folder. + */ selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"), + + /** + * Sends download progress value to the main process (used for taskbar / UI sync). + * + * @param {number} percent - Download progress percentage (0–100) + */ setProgress: (percent) => ipcRenderer.send("set-progress", percent), + + /** + * Retrieves application feature flags (runtime configuration). + */ getFeatures: () => ipcRenderer.invoke("features"), - setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }), + + /** + * Updates a feature flag dynamically at runtime. + * + * @param {string} key - Feature name + * @param {any} value - Feature value + */ + setFeature: (key, value) => + ipcRenderer.invoke("set-feature", { key, value }), + + /** + * Returns the current application version. + */ getVersion: () => ipcRenderer.invoke("version"), - getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path), + + /** + * Validates a download path before using it for file operations. + * + * @param {string} path - Path to validate + */ + getValidatedDownloadPath: (path) => + ipcRenderer.invoke("validate-download-path", path), + + /** + * Retrieves available themes from the filesystem or config layer. + */ getThemes: () => ipcRenderer.invoke("get-themes"), - reloadThemes: () => ipcRenderer.invoke("reload-themes") + + /** + * Forces a reload of theme files (useful after modification/import). + */ + reloadThemes: () => ipcRenderer.invoke("reload-themes"), }); -// Contrôles de fenêtre et outils custom pour la topbar + +/** + * Exposes window control and developer utilities for the custom topbar UI. + * + * These methods forward commands to the Electron main process via IPC. + */ contextBridge.exposeInMainWorld("topbarAPI", { + + /** + * Minimizes the application window. + */ minimize: () => ipcRenderer.send("window-minimize"), + + /** + * Toggles maximize/restore state of the application window. + */ maximize: () => ipcRenderer.send("window-maximize"), + + /** + * Closes the application window. + */ close: () => ipcRenderer.send("window-close"), + + /** + * Opens Chromium DevTools for debugging. + */ openDevTools: () => ipcRenderer.send("open-devtools"), + + /** + * Opens application logs directory or log viewer. + */ openLogs: () => ipcRenderer.send("open-logs"), + + /** + * Opens the official website or project homepage. + */ openWebsite: () => ipcRenderer.send("open-website"), + + /** + * Opens the theme directory or theme editor interface. + */ openTheme: () => ipcRenderer.send("open-theme"), + + /** + * Opens project wiki/documentation. + */ openWiki: () => ipcRenderer.send("open-wiki"), + + /** + * Opens the workshop or external content library. + */ openWorkshop: () => ipcRenderer.send("open-workshop"), - openConfig: () => ipcRenderer.send("open-config") -}); + + /** + * Opens configuration/settings panel. + */ + openConfig: () => ipcRenderer.send("open-config"), +}); \ No newline at end of file diff --git a/public/script/appVersion.js b/public/script/appVersion.js index 6ad1775..8e8f505 100644 --- a/public/script/appVersion.js +++ b/public/script/appVersion.js @@ -1,8 +1,21 @@ -async function versionLabel(){ - const appVersion = await window.electronAPI.getVersion(); +/** + * Retrieves the application version from the Electron main process + * and displays it in the UI version badge (bottom-right corner). + * + * This is mainly used for debugging and build identification. + */ +async function versionLabel() { + const appVersion = await window.electronAPI.getVersion(); - // Write in front the app version for debugging (bottom right) - document.getElementById("version-badge").textContent = `v${appVersion}`; -}; + /** + * UI element displaying the current application version. + * Updated at runtime after IPC call resolves. + */ + const versionBadge = document.getElementById("version-badge"); + + if (!versionBadge) return; + + versionBadge.textContent = `v${appVersion}`; +} versionLabel(); \ No newline at end of file diff --git a/public/script/clipboardPaste.js b/public/script/clipboardPaste.js index 52f2304..5ec227f 100644 --- a/public/script/clipboardPaste.js +++ b/public/script/clipboardPaste.js @@ -1,35 +1,42 @@ -// Clipboard paste functionality for URL input -document.addEventListener('DOMContentLoaded', () => { - const pasteBtn = document.getElementById('pasteBtn'); - const urlInput = document.getElementById('UrlInput'); +/** + * Initializes clipboard paste functionality for the URL input field. + * + * When the user clicks the paste button, the system attempts to read + * the clipboard content and inject it into the URL input field. + * It also triggers an input event to notify any bound listeners. + */ +document.addEventListener("DOMContentLoaded", () => { + const pasteBtn = document.getElementById("pasteBtn"); + const urlInput = document.getElementById("UrlInput"); - if (pasteBtn && urlInput) { - pasteBtn.addEventListener('click', async () => { - try { - // Read text from clipboard - const text = await navigator.clipboard.readText(); - - // Paste into the URL input - urlInput.value = text; - - // Trigger input event to ensure any listeners are notified - urlInput.dispatchEvent(new Event('input', { bubbles: true })); - - // Optional: Focus the input - urlInput.focus(); - - // Visual feedback - pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)'; - setTimeout(() => { - pasteBtn.style.backgroundColor = ''; - }, 200); - - } catch (err) { - console.error('Failed to read clipboard:', err); - - // Fallback: prompt user if clipboard API fails - alert('Unable to access clipboard. Please paste manually using Ctrl+V.'); - } - }); + if (!pasteBtn || !urlInput) return; + + /** + * Handles clipboard paste action triggered by the paste button. + * Reads clipboard text and inserts it into the URL input field. + */ + pasteBtn.addEventListener("click", async () => { + try { + const text = await navigator.clipboard.readText(); + + urlInput.value = text; + + // Notify frameworks / listeners bound to input changes + urlInput.dispatchEvent(new Event("input", { bubbles: true })); + + urlInput.focus(); + + // Temporary UI feedback on paste action + pasteBtn.style.backgroundColor = "var(--form-button-bg-hover-color)"; + setTimeout(() => { + pasteBtn.style.backgroundColor = ""; + }, 200); + } catch (err) { + console.error("Clipboard access failed:", err); + + alert( + "Unable to access clipboard. Please paste manually using Ctrl+V." + ); } -}); + }); +}); \ No newline at end of file diff --git a/public/script/custompath.js b/public/script/custompath.js index a0994ce..7a20751 100644 --- a/public/script/custompath.js +++ b/public/script/custompath.js @@ -1,9 +1,24 @@ +/** + * Initializes the download path system once the UI is ready. + * + * Responsibilities: + * - Restore last known user-selected download path (localStorage) + * - Validate it against backend rules + * - Synchronize with backend authoritative path + * - Setup folder selection UI interaction + */ window.addEventListener("DOMContentLoaded", async () => { const savePathElem = document.getElementById("savePath"); const form = document.getElementById("downloadForm"); - // input caché = DERNIER chemin validé par le back + /** + * Hidden form field storing the validated download path + * used during download submission. + * + * Created dynamically if not already present in DOM. + */ let hidden = document.getElementById("savePathInput"); + if (!hidden) { hidden = document.createElement("input"); hidden.type = "hidden"; @@ -12,6 +27,17 @@ window.addEventListener("DOMContentLoaded", async () => { form.appendChild(hidden); } + /** + * Applies a validated download path received from backend. + * + * Updates: + * - UI display (visible path) + * - tooltip (full path) + * - hidden form input (used for submission) + * - localStorage cache (UX persistence only, not authoritative) + * + * @param {string} path - validated absolute download path + */ async function applyPathFromBack(path) { savePathElem.textContent = path; savePathElem.title = path; @@ -19,6 +45,14 @@ window.addEventListener("DOMContentLoaded", async () => { localStorage.setItem("customDownloadPath", path); // UX only } + /** + * Loads the initial download path on application startup. + * + * Priority order: + * 1. cached localStorage value (UX only, not trusted) + * 2. backend validated version of cached path + * 3. backend default fallback path if validation fails + */ async function loadInitialPath() { // On affiche ce que le user a vu la dernière fois (UX) const cached = localStorage.getItem("customDownloadPath"); @@ -42,13 +76,16 @@ window.addEventListener("DOMContentLoaded", async () => { await loadInitialPath(); - document - .getElementById("changePath") - .addEventListener("click", async () => { + /** + * Opens system folder selector and updates download path. + * + * The selected folder is validated by Electron backend + * before being applied to the UI and persisted. + */ + document.getElementById("changePath").addEventListener("click", async () => { try { // selectDownloadFolder already returns a validated path - const validatedPath = - await window.electronAPI.selectDownloadFolder(); + const validatedPath = await window.electronAPI.selectDownloadFolder(); if (!validatedPath) return; // cancelled diff --git a/public/script/customthemes.js b/public/script/customthemes.js index c9649ac..f52bc86 100644 --- a/public/script/customthemes.js +++ b/public/script/customthemes.js @@ -1,6 +1,22 @@ const themeSelect = document.getElementById("themeSelect"); +/** + * Currently selected themes loaded from backend. + * Used as cache for UI theme switching. + */ let loadedThemes = []; +/** + * Applies a theme object to the application UI by mapping + * theme configuration values to CSS variables and DOM properties. + * + * This function mutates: + * - document.body (background styling) + * - :root CSS variables (design system tokens) + * - subtitle DOM element (theme metadata display) + * + * @param {Object} theme - Theme definition object + * @param {Object} theme.style - Style configuration + */ function applyTheme(theme) { const root = document.documentElement; const style = theme.style; @@ -15,72 +31,75 @@ function applyTheme(theme) { document.body.style.backgroundImage = ""; } - // Colors - root.style.setProperty("--background-color", style.colors?.background || ""); - root.style.setProperty("--default-text-color", style.colors?.text?.default || ""); - root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || ""); - root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || ""); + // CSS variables (design tokens) + root.style.setProperty("--background-color", style.colors?.background || ""); + root.style.setProperty("--default-text-color", style.colors?.text?.default || ""); + root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || ""); + root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || ""); - // Form - root.style.setProperty("--form-bg-color", style.form?.background || ""); - root.style.setProperty("--form-input-bg-color", style.form?.input?.background || ""); - root.style.setProperty("--form-input-border-color", style.form?.input?.border || ""); - root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || ""); - root.style.setProperty("--form-input-text-color", style.form?.input?.text || ""); - root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || ""); - root.style.setProperty("--form-button-bg-color", style.form?.button?.background || ""); - root.style.setProperty("--form-button-text-color", style.form?.button?.text || ""); - root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || ""); - root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || ""); + root.style.setProperty("--form-bg-color", style.form?.background || ""); + root.style.setProperty("--form-input-bg-color", style.form?.input?.background || ""); + root.style.setProperty("--form-input-border-color", style.form?.input?.border || ""); + root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || ""); + root.style.setProperty("--form-input-text-color", style.form?.input?.text || ""); + root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || ""); + root.style.setProperty("--form-button-bg-color", style.form?.button?.background || ""); + root.style.setProperty("--form-button-text-color", style.form?.button?.text || ""); + root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || ""); + root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || ""); - // Checkbox - root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || ""); - root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || ""); - root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || ""); - root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || ""); + root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || ""); + root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || ""); + root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || ""); + root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || ""); - // Download root.style.setProperty("--download-status-color", style.download?.status || ""); - // Progress bar - root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || ""); + root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || ""); root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || ""); - // Video info - root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || ""); - root.style.setProperty("--video-info-text-color", style.videoInfo?.text || ""); - root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || ""); - root.style.setProperty("--video-info-list-color", style.videoInfo?.list || ""); + root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || ""); + root.style.setProperty("--video-info-text-color", style.videoInfo?.text || ""); + root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || ""); + root.style.setProperty("--video-info-list-color", style.videoInfo?.list || ""); root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || ""); - root.style.setProperty("--video-info-link-color", style.videoInfo?.link || ""); - root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || ""); + root.style.setProperty("--video-info-link-color", style.videoInfo?.link || ""); + root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || ""); - // Playlist root.style.setProperty("--playlist-background-color", style.playlist?.background || ""); - // Settings - root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || ""); - root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || ""); - root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || ""); - root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || ""); - root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || ""); - root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || ""); - root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || ""); - root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || ""); - root.style.setProperty("--settings-text-color", style.settings?.text || ""); - root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || ""); + root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || ""); + root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || ""); + root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || ""); + root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || ""); + root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || ""); + root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || ""); + root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || ""); + root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || ""); + root.style.setProperty("--settings-text-color", style.settings?.text || ""); + root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || ""); - // Subtitle const subtitleEl = document.getElementById("subtitle"); if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name; } +/** + * Persists selected theme ID into Electron backend features storage. + * + * @param {string} themeId + */ function saveTheme(themeId) { window.electronAPI.setFeature("theme", themeId); } +/** + * Populates theme dropdown selector with available themes. + * + * @param {Array} themes + */ function populateThemeSelect(themes) { themeSelect.innerHTML = ""; + for (const theme of themes) { const option = document.createElement("option"); option.value = theme.id; @@ -89,35 +108,48 @@ function populateThemeSelect(themes) { } } +/** + * Initializes theme system: + * - loads themes from backend + * - restores saved theme + * - applies selected theme + */ async function initThemes() { loadedThemes = await window.electronAPI.getThemes(); - if (!loadedThemes.length) return; populateThemeSelect(loadedThemes); const features = await window.electronAPI.getFeatures(); const savedId = features.theme; + const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0]; themeSelect.value = theme.id; applyTheme(theme); } +/** + * Handles theme selection change from UI dropdown. + */ themeSelect.addEventListener("change", (e) => { const theme = loadedThemes.find(t => t.id === e.target.value); - if (theme) { - applyTheme(theme); - saveTheme(theme.id); - } + if (!theme) return; + + applyTheme(theme); + saveTheme(theme.id); }); +/** + * Reloads themes from backend and reapplies current selection. + */ async function refreshThemes() { loadedThemes = await window.electronAPI.reloadThemes(); populateThemeSelect(loadedThemes); const features = await window.electronAPI.getFeatures(); const savedId = features.theme; + const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0]; themeSelect.value = theme.id; diff --git a/public/script/downloadstatus.js b/public/script/downloadstatus.js index b6362ec..c5edf62 100644 --- a/public/script/downloadstatus.js +++ b/public/script/downloadstatus.js @@ -2,9 +2,35 @@ const form = document.getElementById("downloadForm"); const statusDiv = document.getElementById("downloadStatus"); const button = form.querySelector("button[type=\"submit\"]"); const stopBtn = document.getElementById("stopBtn"); + +/** + * Indicates whether a download process is currently active. + * Used to prevent duplicate submissions and to gate UI actions + * such as disabling the form or showing progress. + */ let isDownloading = false; + +/** + * Indicates whether the current download was explicitly + * stopped by the user via the cancel action. + * + * Used to differentiate between: + * - user-initiated cancellation + * - failures or natural completion + */ let userStoppedDownload = false; + + +/** + * Handles download form submission. + * + * - Validates user input (URL) + * - Disables UI during download + * - Sends request to backend `/download` + * - Manages success / error states + * - Restores UI state after completion + */ form.addEventListener("submit", async (e) => { e.preventDefault(); @@ -61,6 +87,13 @@ form.addEventListener("submit", async (e) => { } }); +/** + * Displays initial download progress UI immediately + * when a download starts. + * + * Initializes progress bar, speed indicator, stage text + * and shows the stop button. + */ function showInitialProgress() { const progressWrapper = document.getElementById("downloadProgressWrapper"); const progressBarText = document.getElementById("downloadProgressText"); @@ -84,6 +117,14 @@ function showInitialProgress() { if (stopBtn) stopBtn.style.display = "inline-flex"; } +/** + * Cancels the current download process. + * + * Sends a request to backend `/download/cancel`, + * updates UI state and resets progress indicators. + * + * Prevents duplicate calls if no download is active. + */ async function stopDownload() { if (!isDownloading) return; @@ -109,6 +150,12 @@ async function stopDownload() { } } +/** + * Resets all download progress UI elements to their + * default hidden state. + * + * Used after completion or cancellation of a download. + */ function resetProgressBar() { const progressWrapper = document.getElementById("downloadProgressWrapper"); if (progressWrapper) progressWrapper.style.display = "none"; diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js index e992282..4391eb9 100644 --- a/public/script/fetchinfo.js +++ b/public/script/fetchinfo.js @@ -1,12 +1,33 @@ +/** + * Converts a raw upload date string (format YYYYMMDD) + * into a human-readable format (DD/MM/YYYY). + * + * Returns "Inconnue" if the input is invalid or malformed. + */ function formatDate(dateStr) { if (!dateStr || dateStr.length !== 8) return "Inconnue"; return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`; } +/** + * Converts a file size in bytes into megabytes (MB). + * + * Returns a formatted string with 2 decimals (e.g. "12.34 Mo"). + * If input is falsy, returns "Inconnue". + */ function formatSize(bytes) { return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue"; } +/** + * Fetches video or playlist metadata from the backend `/info` endpoint. + * + * Sends a POST request with URL-encoded body. + * Handles network errors, HTTP errors and JSON parsing safely. + * + * @param {string} url - Video or playlist URL + * @returns {Promise} Parsed metadata or error object + */ async function fetchVideoInfo(url) { try { const res = await fetch("/info", { @@ -26,9 +47,19 @@ async function fetchVideoInfo(url) { } } - +/** + * Initializes the video info system UI. + * + * - Listens to URL input changes + * - Automatically fetches video/playlist metadata if enabled + * - Renders either playlist UI or single video UI + * - Handles loading states and caching (lastFetchedUrl) + * + * Also manages dynamic playlist title injection into the form. + */ async function init() { + // Main input listener: triggers auto-fetch when URL changes document.addEventListener("DOMContentLoaded", () => { const urlInput = document.getElementById("UrlInput"); const infoDiv = document.getElementById("videoInfo"); diff --git a/public/script/progressBar.js b/public/script/progressBar.js index 4b8e5ce..0cbb082 100644 --- a/public/script/progressBar.js +++ b/public/script/progressBar.js @@ -1,40 +1,65 @@ +/** + * DOM elements used for download progress UI rendering. + * These elements are updated in real-time via SSE streams. + */ const progressWrapper = document.getElementById("downloadProgressWrapper"); const progressBar = document.getElementById("downloadProgress"); -const progressBarText = document.getElementById("downloadProgressText") +const progressBarText = document.getElementById("downloadProgressText"); const speedElement = document.getElementById("downloadSpeedText"); const stageElement = document.getElementById("downloadStage"); const playlistInfoElement = document.getElementById("playlistInfoText"); + +/** + * Server-Sent Events streams for real-time download updates. + * Each stream is responsible for a specific aspect of the download state. + */ const speedEvt = new EventSource("/download/speed"); const stageEvt = new EventSource("/download/stage"); const playlistInfoEvt = new EventSource("/download/playlist-info"); +/** + * Resets the progress UI to its initial state. + * Hides all dynamic elements and clears displayed values. + */ function startProgress() { progressBar.style.width = "0%"; progressBarText.innerHTML = "0%"; speedElement.innerHTML = "0 Mib/s"; } +/** + * Makes the progress UI visible and enables related controls + * such as the stop button. + */ function showProgress() { progressWrapper.style.display = "block"; progressBarText.style.display = "block"; speedElement.style.display = "block"; - // Show stop button const stopBtn = document.getElementById("stopBtn"); if (stopBtn) stopBtn.style.display = "inline-flex"; } +/** + * Updates the progress bar UI with the given percentage. + * Automatically triggers UI visibility if not already shown. + * + * @param {number} percent - Download progress percentage (0–100) + */ function updateProgress(percent) { - // Show progress div and stop button only when percent > 0 if (percent > 0 && progressWrapper.style.display !== "block") { showProgress(); } - + progressBar.style.width = `${percent}%`; progressBarText.innerHTML = `${percent}%`; } +/** + * Fully resets the progress UI and hides all download indicators. + * Called after completion or cancellation. + */ function resetProgress() { progressBar.style.width = "0%"; progressBarText.textContent = ""; @@ -50,13 +75,19 @@ function resetProgress() { playlistInfoElement.textContent = ""; playlistInfoElement.style.display = "none"; - // Hide stop button const stopBtn = document.getElementById("stopBtn"); if (stopBtn) stopBtn.style.display = "none"; } -// SSE Connexion +/** + * Main SSE stream handling download progress updates. + * Supports: + * - reset signal + * - done signal + * - percentage updates + */ const evtSource = new EventSource("/download/progress"); + evtSource.onmessage = e => { if (e.data === "reset") { startProgress(); @@ -65,34 +96,41 @@ evtSource.onmessage = e => { } if (e.data === "done") { - // Keep progress bar visible for better UX, let toast handle the feedback setTimeout(() => { resetProgress(); - window.electronAPI.setProgress(-1); - }, 2000); // Wait 2s so user sees 100% progress + window.electronAPI.setProgress(-1); + }, 2000); return; } const percent = parseFloat(e.data); - + if (!isNaN(percent)) { updateProgress(percent); - window.electronAPI.setProgress(percent); // Update Task Bar - // Don't hide at 100%, wait for "done" signal instead + window.electronAPI.setProgress(percent); } }; +/** + * SSE stream: download speed updates (e.g. "5.2 MiB/s"). + */ speedEvt.onmessage = e => { speedElement.style.display = "block"; - speedElement.textContent = e.data; // ex: "5.2MiB/s" + speedElement.textContent = e.data; }; +/** + * SSE stream: download stage updates (e.g. downloading, merging, extracting). + */ stageEvt.onmessage = e => { stageElement.style.display = "block"; - stageElement.textContent = e.data; // ex: "📥 Downloading..." + stageElement.textContent = e.data; }; +/** + * SSE stream: playlist progress updates (e.g. "Item 1 of 15"). + */ playlistInfoEvt.onmessage = e => { playlistInfoElement.style.display = "block"; - playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15" + playlistInfoElement.textContent = e.data; }; \ No newline at end of file diff --git a/public/script/settingsPanel.js b/public/script/settingsPanel.js index b5a1168..528679d 100644 --- a/public/script/settingsPanel.js +++ b/public/script/settingsPanel.js @@ -1,66 +1,105 @@ +/** + * Settings panel DOM elements used to control application feature flags + * and configuration UI interactions. + */ const settingsPanel = document.getElementById("settings-panel"); const settingsModal = document.querySelector(".settings-modal"); - const settingsButton = document.getElementById("settings-button"); +/** + * Tracks current settings panel state. + * NOTE: actual visibility is derived from DOM style in current implementation. + */ let isOpen = false; +/** + * Toggles visibility of the settings panel when clicking the settings button. + */ settingsButton.addEventListener("click", () => { - const isOpen = settingsPanel.style.display === "block"; - settingsPanel.style.display = isOpen ? "none" : "block"; + const currentlyOpen = settingsPanel.style.display === "block"; + settingsPanel.style.display = currentlyOpen ? "none" : "block"; }); +/** + * Closes the settings panel and resets internal state. + */ function closePanel() { - isOpen = false; - settingsPanel.style.display = "none"; + isOpen = false; + settingsPanel.style.display = "none"; } - +/** + * Loads feature flags from Electron backend and syncs them + * with corresponding UI controls (checkboxes, selects). + * + * Also registers change listeners to persist updates in real time. + */ async function loadSettings() { const features = await window.electronAPI.getFeatures(); - document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => { - const key = el.dataset.key; - if (features[key] !== undefined) { - if (el.type === "checkbox") el.checked = features[key]; - else if (el.tagName === "SELECT") el.value = features[key]; - } + document + .querySelectorAll(".settings-section input, .settings-section select") + .forEach((el) => { + const key = el.dataset.key; + if (!key) return; - el.addEventListener("change", () => { - if (key === "theme") return; - let value = el.type === "checkbox" ? el.checked : el.value; - window.electronAPI.setFeature(key, value); + if (features[key] !== undefined) { + if (el.type === "checkbox") el.checked = features[key]; + else if (el.tagName === "SELECT") el.value = features[key]; + } + + el.addEventListener("change", () => { + if (key === "theme") return; + + const value = el.type === "checkbox" ? el.checked : el.value; + window.electronAPI.setFeature(key, value); + }); }); - }); } -// Open The JSON -document.getElementById("open-json-btn").addEventListener("click", () => { - window.topbarAPI.openConfig(); +/** + * Opens configuration JSON file via Electron topbar API. + */ +document.getElementById("open-json-btn") + ?.addEventListener("click", () => { + window.topbarAPI.openConfig(); + }); + +/** + * Opens theme directory via Electron topbar API. + */ +document.getElementById("open-theme") + ?.addEventListener("click", () => { + window.topbarAPI.openTheme(); + }); + +/** + * Refreshes theme list and updates UI selector with a short animation. + */ +document.getElementById("refresh-themes-btn") + ?.addEventListener("click", function () { + this.classList.add("spinning"); + window.refreshThemes(); + + setTimeout(() => { + this.classList.remove("spinning"); + }, 600); + }); + +/** + * Clicking the overlay outside the modal closes the settings panel. + */ +settingsPanel?.addEventListener("click", closePanel); + +/** + * Prevents modal clicks from closing the settings panel + * by stopping event propagation to the overlay. + */ +settingsModal?.addEventListener("click", (e) => { + e.stopPropagation(); }); -// Open Theme Folder -document.getElementById("open-theme").addEventListener("click", () => { - window.topbarAPI.openTheme(); -}); - -// Refresh Themes -document.getElementById("refresh-themes-btn").addEventListener("click", function() { - this.classList.add("spinning"); - window.refreshThemes(); - - setTimeout(() => { - this.classList.remove("spinning"); - }, 600); -}); - - -/* clic sur l'overlay = fermer */ -settingsPanel.addEventListener("click", closePanel); - -/* clic dans la modal = stop propagation */ -settingsModal.addEventListener("click", (e) => { - e.stopPropagation(); -}); - -loadSettings(); +/** + * Initializes settings UI by loading backend feature flags. + */ +loadSettings(); \ No newline at end of file diff --git a/public/script/toast.js b/public/script/toast.js index 0cb3822..56afa60 100644 --- a/public/script/toast.js +++ b/public/script/toast.js @@ -1,17 +1,26 @@ -window.showToast = function(message, type = 'info', duration = 4000) { - const container = document.getElementById('toast-container'); - if (!container) return; +/** + * Displays a toast notification in the global UI container. + * + * Toasts are automatically appended to `#toast-container`, + * support multiple visual types, and can auto-dismiss after a delay. + * + * @param {string} message - Message displayed inside the toast + * @param {"success"|"error"|"warning"|"info"} [type="info"] - Visual style of the toast + * @param {number} [duration=4000] - Auto-dismiss delay in milliseconds (0 disables auto removal) + * @returns {HTMLElement|null} The created toast element or null if container is missing + */ +window.showToast = function (message, type = "info", duration = 4000) { + const container = document.getElementById("toast-container"); + if (!container) return null; - // Create toast element - const toast = document.createElement('div'); - toast.classList.add('toast', type); + const toast = document.createElement("div"); + toast.classList.add("toast", type); - // Icons for different types const icons = { - success: '✓', - error: '✕', - warning: '⚠', - info: 'ℹ' + success: "✓", + error: "✕", + warning: "⚠", + info: "ℹ" }; const icon = icons[type] || icons.info; @@ -22,14 +31,11 @@ window.showToast = function(message, type = 'info', duration = 4000) { `; - // Add close functionality - const closeBtn = toast.querySelector('.toast-close'); - closeBtn.addEventListener('click', () => removeToast(toast)); + const closeBtn = toast.querySelector(".toast-close"); + closeBtn.addEventListener("click", () => removeToast(toast)); - // Add to container container.appendChild(toast); - // Auto remove after duration if (duration > 0) { setTimeout(() => removeToast(toast), duration); } @@ -37,18 +43,41 @@ window.showToast = function(message, type = 'info', duration = 4000) { return toast; }; +/** + * Removes a toast element with a small exit animation. + * + * @param {HTMLElement} toast + */ function removeToast(toast) { - if (!toast.parentElement) return; - - toast.classList.add('removing'); + if (!toast?.parentElement) return; + + toast.classList.add("removing"); + setTimeout(() => { - if (toast.parentElement) { - toast.remove(); - } + toast.remove(); }, 300); } -window.showSuccess = (message, duration = 4000) => window.showToast(message, 'success', duration); -window.showError = (message, duration = 5000) => window.showToast(message, 'error', duration); -window.showWarning = (message, duration = 4000) => window.showToast(message, 'warning', duration); -window.showInfo = (message, duration = 4000) => window.showToast(message, 'info', duration); +/** + * Shortcut: success toast + */ +window.showSuccess = (message, duration = 4000) => + window.showToast(message, "success", duration); + +/** + * Shortcut: error toast + */ +window.showError = (message, duration = 5000) => + window.showToast(message, "error", duration); + +/** + * Shortcut: warning toast + */ +window.showWarning = (message, duration = 4000) => + window.showToast(message, "warning", duration); + +/** + * Shortcut: info toast + */ +window.showInfo = (message, duration = 4000) => + window.showToast(message, "info", duration); \ No newline at end of file diff --git a/public/script/topbar.js b/public/script/topbar.js index a13e000..8c4e496 100644 --- a/public/script/topbar.js +++ b/public/script/topbar.js @@ -1,16 +1,22 @@ -// Ajout des listeners pour la topbar +/** + * Initializes topbar button event listeners and binds them + * to the Electron preload API exposed in `window.topbarAPI`. + * + * This function is safe to call once DOM is ready. + * It silently aborts if the API or elements are missing. + */ function setupTopbarListeners() { const { topbarAPI } = window; if (!topbarAPI) return; - - const minBtn = document.getElementById('minimize-btn'); - const maxBtn = document.getElementById('maximize-btn'); - const closeBtn = document.getElementById('close-btn'); - const devtoolsBtn = document.getElementById('devtools-btn'); - const logsBtn = document.getElementById('logs-btn'); - const websiteBtn = document.getElementById('website-btn'); - const wikiBtn = document.getElementById('wiki-btn'); - const workshopBtn = document.getElementById('workshop-btn'); + + const minBtn = document.getElementById("minimize-btn"); + const maxBtn = document.getElementById("maximize-btn"); + const closeBtn = document.getElementById("close-btn"); + const devtoolsBtn = document.getElementById("devtools-btn"); + const logsBtn = document.getElementById("logs-btn"); + const websiteBtn = document.getElementById("website-btn"); + const wikiBtn = document.getElementById("wiki-btn"); + const workshopBtn = document.getElementById("workshop-btn"); if (minBtn) minBtn.onclick = () => topbarAPI.minimize(); if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize(); @@ -22,12 +28,30 @@ function setupTopbarListeners() { if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop(); } -document.addEventListener('DOMContentLoaded', setupTopbarListeners); +/** + * Registers topbar event listeners after DOM is fully loaded. + */ +document.addEventListener("DOMContentLoaded", setupTopbarListeners); -const features = await window.electronAPI.getFeatures(); +/** + * Applies UI layout adjustments depending on feature flags + * provided by the Electron backend. + * + * Specifically handles: + * - hiding custom topbar + * - adjusting layout spacing + * - repositioning theme switcher + */ +(async function applyFeatureLayout() { + const features = await window.electronAPI.getFeatures(); -if(!features.customTopBar){ - document.getElementById("topbar").style.display = "none"; - document.getElementById("container").style.marginTop = "0"; - document.getElementById("theme-switcher").style.top = "30px"; -} \ No newline at end of file + if (!features.customTopBar) { + const topbar = document.getElementById("topbar"); + const container = document.getElementById("container"); + const themeSwitcher = document.getElementById("theme-switcher"); + + if (topbar) topbar.style.display = "none"; + if (container) container.style.marginTop = "0"; + if (themeSwitcher) themeSwitcher.style.top = "30px"; + } +})(); \ No newline at end of file diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js index edb0e81..2b3956f 100644 --- a/server/controller/download.controller.js +++ b/server/controller/download.controller.js @@ -4,11 +4,47 @@ const { isValidUrl, isSafePath } = require("../helpers/validation.helpers"); const { notifyDownloadFinished } = require("../helpers/notify.helpers"); const { configFeatures } = require("../../config"); -const listeners = []; -const speedListeners = []; -const stageListeners = []; -const playlistInfoListeners = []; +/** + * Active Server-Sent Event subscribers receiving + * download progress updates. + * + * Each listener receives a percentage value between + * 0 and 100 during the download lifecycle. + */ +const progressListeners = []; +/** + * Active Server-Sent Event subscribers receiving + * download speed updates. + */ +const speedListeners = []; + +/** + * Active Server-Sent Event subscribers receiving + * download stage updates. + */ +const stageListeners = []; + +/** + * Active Server-Sent Event subscribers receiving + * playlist progress information. + */ +const playlistListeners = []; + +/** + * Handles download requests and starts a yt-dlp download. + * + * Validates user input, prepares download options and + * delegates the download process to the download service. + * + * Once the download completes successfully, a desktop + * notification may be displayed depending on the + * application configuration. + * + * @param {import("express").Request} req + * @param {import("express").Response} res + * @returns {Promise} + */ async function downloadController(req, res) { try { const options = { @@ -25,17 +61,30 @@ async function downloadController(req, res) { return res.status(400).send("Save Path Not Allowed."); } - // Get output folder when the download is finished - const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners); + const outputFolder = await fetchDownload( + options, + progressListeners, + speedListeners, + stageListeners, + playlistListeners + ); + notifyDownloadFinished(outputFolder, configFeatures.notifySystem); res.send("Download Done !"); } catch (err) { - logger.error(`Server Error in /download : ${err.message}`); - res.status(500).send(`Server Error`); + logger.error(`Server Error in /download: ${err?.message ?? err}`); + res.status(500).send("Server Error"); } } +/** + * Opens a Server-Sent Events (SSE) connection used to + * stream download progress updates to the client. + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ function progressController(req, res) { res.set({ 'Content-Type': 'text/event-stream', @@ -44,14 +93,21 @@ function progressController(req, res) { }); const sendProgress = percent => res.write(`data: ${percent}\n\n`); - listeners.push(sendProgress); + progressListeners.push(sendProgress); req.on('close', () => { - listeners.splice(listeners.indexOf(sendProgress), 1); + progressListeners.splice(progressListeners.indexOf(sendProgress), 1); res.end(); }); } +/** + * Opens a Server-Sent Events (SSE) connection used to + * stream download speed updates to the client. + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ function speedController(req, res) { res.set({ 'Content-Type': 'text/event-stream', @@ -68,6 +124,14 @@ function speedController(req, res) { }); } +/** + * Opens a Server-Sent Events (SSE) connection used to + * stream download stage updates such as downloading, + * merging, extracting audio or embedding metadata. + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ function stageController(req, res) { res.set({ 'Content-Type': 'text/event-stream', @@ -84,6 +148,14 @@ function stageController(req, res) { }); } +/** + * Cancels the currently running download process. + * + * Returns an error response if no download is active. + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ function cancelDownloadController(req, res) { const cancelled = cancelDownload(); if (cancelled) { @@ -95,6 +167,18 @@ function cancelDownloadController(req, res) { } } +/** + * Opens a Server-Sent Events (SSE) connection used to + * stream playlist progress information to the client. + * + * Example: + * - Item 1 of 20 + * - Item 12 of 20 + * - Item 20 of 20 + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ function playlistInfoController(req, res) { res.set({ 'Content-Type': 'text/event-stream', @@ -103,10 +187,10 @@ function playlistInfoController(req, res) { }); const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`); - playlistInfoListeners.push(sendPlaylistInfo); + playlistListeners.push(sendPlaylistInfo); req.on('close', () => { - playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1); + playlistListeners.splice(playlistListeners.indexOf(sendPlaylistInfo), 1); res.end(); }); } diff --git a/server/controller/info.controller.js b/server/controller/info.controller.js index 57f42b3..d8078ac 100644 --- a/server/controller/info.controller.js +++ b/server/controller/info.controller.js @@ -3,70 +3,114 @@ const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers"); const { logger } = require("../logger"); const { isValidUrl } = require("../helpers/validation.helpers"); -async function infoController(req, res){ +/** + * Handles metadata retrieval for a video or playlist. + * + * This controller: + * - Validates incoming URL (query or body) + * - Calls yt-dlp metadata extraction service + * - Detects playlist vs single video context + * - Applies fallback strategy if playlist extraction fails + * - Returns normalized parsed response (video or playlist) + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ +async function infoController(req, res) { - const url = req.body.url || req.query.url; // POST et GET + const url = req.body.url || req.query.url; // Supports POST and GET requests - - // If no url, non-string url or invalid url - - if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing"); - - logger.info(`/Info Request receive by the Info Controller. URL: ${url}`); - - if (url.includes("&list") || url.includes("?list")) { - logger.info("Estimated Data Type: Playlist") - - } else{ - logger.info("Estimated Data Type: Video") + /** + * Basic validation: + * - URL must exist + * - Must be a string + * - Must pass custom URL validation + */ + if (!url || typeof url !== "string" || !isValidUrl(url)) { + return res.status(400).send("Invalid URL Or Missing"); } - try { - let data; - let fetchError = null; + logger.info(`Info request received. URL: ${url}`); + + // Lightweight heuristic to detect playlist URLs + const isPlaylistUrl = url.includes("&list") || url.includes("?list"); + + logger.info( + isPlaylistUrl + ? "Estimated Data Type: Playlist" + : "Estimated Data Type: Video" + ); try { - data = await fetchInfo(url); - } catch (err) { - fetchError = err; - - // If it's a playlist URL and fetch failed, try fetching just the single video - if ((url.includes("&list") || url.includes("?list"))) { - logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`); - - // Remove the &list parameter - const singleVideoUrl = url.split("&list")[0].split("?list")[0]; - - try { - data = await fetchInfo(singleVideoUrl); - logger.info(`Single video fetch succeeded after playlist failure`); - } catch (retryErr) { - logger.error(`Single video fetch also failed: ${retryErr.message}`); - throw err; // Throw the original error + let data; + + /** + * Primary metadata fetch attempt + */ + try { + data = await fetchInfo(url); + } catch (err) { + + /** + * Fallback strategy: + * If playlist extraction fails, retry as single video + */ + if (isPlaylistUrl) { + + logger.info( + `Playlist fetch failed: ${err.message}. Retrying as single video...` + ); + + // Remove playlist query parameter (best-effort cleanup) + const singleVideoUrl = url.split("&list")[0].split("?list")[0]; + + try { + data = await fetchInfo(singleVideoUrl); + logger.info("Single video fetch succeeded after playlist failure"); + } catch (retryErr) { + logger.error( + `Single video fetch also failed: ${retryErr.message}` + ); + + // Preserve original error context + throw err; + } + + } else { + throw err; } - } else { - throw err; } - } - if (data._type === "playlist") { + /** + * Normalize response based on yt-dlp output type + */ + if (data._type === "playlist") { - const playlist = parsePlaylist(data); - logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`); - return res.json(playlist); + const playlist = parsePlaylist(data); - } else{ - const video = parseVideo(data); - logger.info(`Unique Video: ${video.title}`); - return res.json(video); - } + logger.info( + `Playlist fetched: ${playlist.title} (${playlist.count} videos)` + ); + return res.json(playlist); + + } else { + + const video = parseVideo(data); + + logger.info(`Video fetched: ${video.title}`); + + return res.json(video); + } } catch (err) { - logger.error(`Info controller error: ${err && err.message ? err.message : err}`); - return res.status(500).send(`Unable to fetch info`); + + logger.error( + `Info controller error: ${err && err.message ? err.message : err}` + ); + + return res.status(500).send("Unable to fetch info"); } - } -module.exports = {infoController}; \ No newline at end of file +module.exports = { infoController }; \ No newline at end of file diff --git a/server/helpers/buildArgs.helpers.js b/server/helpers/buildArgs.helpers.js index 4b8432d..45370f0 100644 --- a/server/helpers/buildArgs.helpers.js +++ b/server/helpers/buildArgs.helpers.js @@ -5,6 +5,26 @@ const { ffmpegPath, denoPath} = require("./path.helpers.js"); const { configFeatures } = require("../../config.js"); const { logger } = require("../logger.js"); +/** + * Validates whether the provided codec is supported by the application. + * + * If the codec is supported, it is returned unchanged. + * Otherwise, a warning is logged and the default codec ("h264") + * is returned as a fallback. + * + * Supported codecs: + * - av01 + * - vp9.2 + * - vp9 + * - h265 + * - h264 + * - vp8 + * - h263 + * - theora + * + * @param {string} codec - Codec identifier to validate. + * @returns {string} A valid codec identifier. + */ function validateCodec(codec){ const validCodec = [ @@ -20,6 +40,28 @@ function validateCodec(codec){ } } +/** + * Builds the complete yt-dlp argument list based on the + * application configuration and download options. + * + * The generated arguments include: + * - Browser cookie extraction + * - FFmpeg integration + * - Deno runtime configuration + * - Metadata and thumbnail embedding + * - Playlist handling + * - Video/audio format selection + * - Quality filtering + * - Output file destination + * + * @param {Object} options - Download configuration. + * @param {string} options.url - Video or playlist URL. + * @param {boolean} options.audioOnly - Whether to extract audio only. + * @param {string} options.quality - Requested quality preset. + * @param {string} options.outputFolder - Download destination folder. + * + * @returns {string[]} Array of arguments ready to be passed to yt-dlp. + */ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) { logger.info("--- CONFIGURATION ---"); diff --git a/server/helpers/getBrowser.helpers.js b/server/helpers/getBrowser.helpers.js index 5ee3c34..2610a6d 100644 --- a/server/helpers/getBrowser.helpers.js +++ b/server/helpers/getBrowser.helpers.js @@ -4,10 +4,33 @@ const path = require("path"); const notify = require("./notify.helpers") const { logger } = require("../logger"); +/** + * Detects the first supported browser installed on the system. + * + * The detected browser is used by yt-dlp to import authentication + * cookies when required by certain platforms. + * + * Currently, Firefox is the only browser officially supported + * by the application. + * + * If no supported browser is found: + * - A warning is logged + * - A notification is displayed to the user + * - "firefox" is returned as a fallback to allow yt-dlp to + * handle the error gracefully without crashing the application + * + * @returns {string} The detected browser identifier. + */ function getUserBrowser() { const userProfile = os.homedir(); - // List of browsers supported by yt-dlp (Currently I can only guarantee Firefox, Sorry) + /** + * Browsers supported by yt-dlp cookie extraction. + * + * Only Firefox is currently enabled and tested. + * Additional browsers can be enabled in the future + * once compatibility has been verified. + */ const browsers = [ { name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") }, // { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") }, @@ -19,7 +42,7 @@ function getUserBrowser() { // { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") } ]; - // Search for an available browser + // Search for the first available browser profile. for (const browser of browsers) { if (fs.existsSync(browser.path)) { logger.info(`Browser found: ${browser.name}`); @@ -27,12 +50,12 @@ function getUserBrowser() { } } - // No browser found - notify user + // No supported browser found => Notify User logger.warn("No supported browser found on the system"); notify.notifyFirefoxBrowserMissing(); - // Fallback: return "Firefox" to let YT-DLP manage error - // this avoid app crash + // Fallback to Firefox and let yt-dlp handle the error gracefully. + // This prevents the application from crashing return "firefox"; } diff --git a/server/helpers/notify.helpers.js b/server/helpers/notify.helpers.js index 20ef59e..e614720 100644 --- a/server/helpers/notify.helpers.js +++ b/server/helpers/notify.helpers.js @@ -1,8 +1,19 @@ const { Notification, shell } = require("electron"); const { iconPaths } = require("./path.helpers"); +const { logger } = require("../logger"); +/** + * Displays a system notification when a download completes successfully. + * + * If enabled, clicking the notification opens the download folder + * using the system file explorer. + * + * @param {string} folder - Path to the completed download folder. + * @param {boolean} notifyEnabled - Whether notifications are enabled in config. + */ function notifyDownloadFinished(folder, notifyEnabled = true) { if (!notifyEnabled) return; + if (!folder) return; const notif = new Notification({ title: "Freedom Loader", @@ -14,6 +25,15 @@ function notifyDownloadFinished(folder, notifyEnabled = true) { notif.show(); } +/** + * Displays a notification when browser cookies cannot be extracted. + * + * This usually indicates: + * - User is not logged in to the browser + * - Browser profile is inaccessible + * + * Clicking the notification opens a tutorial link. + */ function notifyCookiesBrowserError(){ const notif = new Notification({ title: "Cookies Error", @@ -21,23 +41,41 @@ function notifyCookiesBrowserError(){ icon: iconPaths.error, }); - notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1")); + notif.on("click", () => + shell.openExternal( + "https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1" + ) + ); + notif.show(); } -function notifyFirefoxBrowserMissing(){ +/** + * Displays a notification when Firefox is not detected + * on the user system. + * + * This is required for cookie extraction via yt-dlp. + * + * Clicking the notification opens an installation guide. + */ +function notifyFirefoxBrowserMissing() { const notif = new Notification({ title: "Firefox Missing", body: "Firefox was not found on your system. Click here to follow the installation guide", icon: iconPaths.error, }); - notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1")) + notif.on("click", () => + shell.openExternal( + "https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1" + ) + ); + notif.show(); } module.exports = { - notifyDownloadFinished, + notifyDownloadFinished, notifyCookiesBrowserError, - notifyFirefoxBrowserMissing + notifyFirefoxBrowserMissing, }; \ No newline at end of file diff --git a/server/helpers/parseInfo.helpers.js b/server/helpers/parseInfo.helpers.js index 6449061..e17e534 100644 --- a/server/helpers/parseInfo.helpers.js +++ b/server/helpers/parseInfo.helpers.js @@ -1,27 +1,38 @@ +/** + * Normalizes a video payload by attaching the resource type. + * + * @param {Object} data - Raw video metadata. + * @returns {Object} Normalized video object. + */ function parseVideo(data) { - - return { - type: "video", - ...data - }; + return { + type: "video", + ...data + }; } +/** + * Transforms a raw playlist payload into a normalized structure + * containing playlist metadata and a simplified list of videos. + * + * @param {Object} data - Raw playlist metadata. + * @returns {Object} Normalized playlist object. + */ function parsePlaylist(data) { - - return { - type: "playlist", - title: data.title || data.id, - channel: data.uploader, - count: data.entries.length, - videos: (data.entries || []).map(v => ({ - id: v.id, - title: v.title, - url: v.webpage_url, - duration: v.duration, - thumbnail: v.thumbnail, - uploader: v.uploader - })) - }; + return { + type: "playlist", + title: data.title || data.id, + channel: data.uploader, + count: data.entries.length, + videos: (data.entries || []).map(v => ({ + id: v.id, + title: v.title, + url: v.webpage_url, + duration: v.duration, + thumbnail: v.thumbnail, + uploader: v.uploader + })) + }; } -module.exports = { parseVideo, parsePlaylist }; +module.exports = { parseVideo, parsePlaylist }; \ No newline at end of file diff --git a/server/helpers/path.helpers.js b/server/helpers/path.helpers.js index b41cf8d..95114d9 100644 --- a/server/helpers/path.helpers.js +++ b/server/helpers/path.helpers.js @@ -14,13 +14,23 @@ const resourcesPath = config.devMode // Default download folder (centralized) const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader"); -// Binary paths +/** + * Runtime-resolved binary paths. + * These values differ between development and production builds. + */ + let userYtDlp; let ffmpegPath; let denoPath; +/** Source binary bundled inside application resources (used for first-time copy) */ const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe"); +/** + * Resolve binary locations depending on environment: + * - dev: local resources folder + * - prod: extracted userData + packaged resources + */ if (config.devMode) { userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe @@ -35,14 +45,20 @@ if (config.devMode) { } } -// Notification icon paths +/** + * Centralized notification and UI icon paths. + * Used by Electron notifications and UI components. + */ const iconPaths = { confirm: path.join(resourcesPath, "confirm-icon.png"), error: path.join(resourcesPath, "error.png"), app: path.join(resourcesPath, "app-icon.ico") }; -// Binary paths for verification +/** + * Static reference paths to bundled binaries. + * Used mainly for verification and diagnostics. + */ const binaryPaths = { ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"), ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"), @@ -50,10 +66,21 @@ const binaryPaths = { deno: path.join(resourcesPath, "binaries", "deno.exe") }; -// Theme paths - themes stored in AppData to survive updates +/** + * Theme storage system: + * - default themes come from app resources + * - user themes are persisted in userData to survive updates + */ const userThemesPath = path.join(app.getPath("userData"), "themes"); const defaultThemesSourcePath = path.join(resourcesPath, "theme"); +/** + * Initializes user theme directory and ensures default themes exist. + * + * - Creates user themes folder if missing + * - Copies bundled themes from resources if not present + * - Applies permissive permissions for runtime modification + */ function initUserThemes() { try { if (!fs.existsSync(userThemesPath)) { @@ -94,6 +121,13 @@ function initUserThemes() { } } +/** + * Recursively copies a directory and its contents. + * + * @param {string} src - Source directory + * @param {string} dest - Destination directory + * @throws Error If copy operation fails + */ function copyDirectory(src, dest) { try { if (!fs.existsSync(dest)) { @@ -116,8 +150,9 @@ function copyDirectory(src, dest) { } } -if (!userYtDlp){ logger.error("Missing YT-DLP")} -if (!ffmpegPath){ logger.error("Missing FFMPEG")} -if (!denoPath){ logger.error("Missing DENO")} +// Runtime validation of critical binaries +if (!userYtDlp){ logger.error("Missing yt-dlp binary")} +if (!ffmpegPath){ logger.error("Missing ffmpeg binary")} +if (!denoPath){ logger.error("Missing deno binary")} module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes }; diff --git a/server/helpers/rateLimit.helpers.js b/server/helpers/rateLimit.helpers.js index 8d85534..e3d57a3 100644 --- a/server/helpers/rateLimit.helpers.js +++ b/server/helpers/rateLimit.helpers.js @@ -1,14 +1,24 @@ const RateLimit = require("express-rate-limit"); -/* Rate Limite */ -const rateLimite = RateLimit({ +/** + * Rate limiter middleware used to protect public endpoints + * against abusive or excessive requests. + * + * Restrictions: + * - 5 requests per IP address + * - 15-minute sliding window + * + * When the limit is exceeded, a HTTP 429 (Too Many Requests) + * response is returned. + */ +const rateLimit = RateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // limit each IP to 100 requests per windowMs on the root path + max: 5, message: "Too many requests, please try again later.", - statusCode: 429, // HTTP status code for "Too Many Requests" + statusCode: 429, }); module.exports = { - rateLimite + rateLimit } \ No newline at end of file diff --git a/server/helpers/validation.helpers.js b/server/helpers/validation.helpers.js index b7e3da1..bca724e 100644 --- a/server/helpers/validation.helpers.js +++ b/server/helpers/validation.helpers.js @@ -1,21 +1,43 @@ const path = require("path"); -function isValidUrl(url) { - try { - new URL(url); - return true; - - } catch { - return false; - } +/** + * Validates whether the provided string is a well-formed URL. + * + * The validation relies on the native URL constructor and + * returns `true` only if the value can be successfully parsed. + * + * @param {string} url - URL to validate. + * @returns {boolean} True if the URL is valid, otherwise false. + */ +function isValidUrl(url) { + try { + new URL(url); + return true; + } catch { + return false; + } } +/** + * Validates whether a folder path is considered safe for + * file operations. + * + * Security checks: + * - Rejects empty or very short paths. + * - Resolves the absolute path before validation. + * - Prevents access to sensitive Windows system directories. + * + * @param {string} folder - Folder path to validate. + * @returns {boolean} True if the path is considered safe, otherwise false. + */ function isSafePath(folder) { if (!folder || folder.length < 3) return false; + const unsafe = ["System32", "\\Windows"]; const resolved = path.resolve(folder); + return !unsafe.some(u => resolved.includes(u)); } -module.exports = { isValidUrl, isSafePath }; +module.exports = { isValidUrl, isSafePath }; \ No newline at end of file diff --git a/server/logger.js b/server/logger.js index 9f4f2cf..3ed6724 100644 --- a/server/logger.js +++ b/server/logger.js @@ -20,7 +20,15 @@ const logFormat = format.combine( format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`) ); -// Logger configuration +/** + * Logger Instance with differents types + * - info + * - error + * - warn + * + * You need to specify the text that need to be displayed + * @param string + */ const logger = createLogger({ level: "info", format: logFormat, @@ -40,11 +48,17 @@ const logger = createLogger({ ], }); +/** + * Start Log Session + */ function logSessionStart() { logger.info(`--- Starting session: ${new Date().toISOString()} ---`); logger.info(`Application Version: ${config.version}`) } +/** + * Stop Log Session + */ function logSessionEnd() { logger.info(`--- Ending session: ${new Date().toISOString()} ---`); } diff --git a/server/server.js b/server/server.js index a31e6ec..f5a5ed0 100644 --- a/server/server.js +++ b/server/server.js @@ -2,7 +2,7 @@ const express = require("express"); const path = require("path"); const { logger, logSessionEnd } = require("./logger"); const config = require("../config"); -const { rateLimite } = require("./helpers/rateLimit.helpers"); +const { rateLimit } = require("./helpers/rateLimit.helpers"); const app = express(); @@ -14,10 +14,18 @@ app.use(express.static(path.join(__dirname, "../public"))); // Routes app.use("/download", require("./routes/download.route")); app.use("/info", require("./routes/info.route")); -app.get("/", rateLimite, (req, res) => { +app.get("/", rateLimit, (req, res) => { res.sendFile(path.join(__dirname, "../public/index.html")); }); +/** + * Initializes and starts the Express HTTP server. + * + * - Binds the server to `config.applicationPort` + * - Resolves with the HTTP server instance on successful startup + * - Rejects on server binding or runtime errors + * - Registers SIGINT/SIGTERM handlers for graceful shutdown + */ async function startServer() { return new Promise((resolve, reject) => { const server = app.listen(config.applicationPort, () => { @@ -30,6 +38,12 @@ async function startServer() { reject(err); }); + /** + * Clean exit function + * - Stop Log + * - Stop Express Server + * - Stop Electron App + */ const gracefulExit = () => { logSessionEnd(); server.close(() => { diff --git a/server/services/download.services.js b/server/services/download.services.js index b80d176..05fb0b1 100644 --- a/server/services/download.services.js +++ b/server/services/download.services.js @@ -8,13 +8,45 @@ const path = require("path"); const { isSafePath } = require("../helpers/validation.helpers"); const { configFeatures } = require("../../config.js"); +/** + * Reference to the currently running yt-dlp process. + * + * Used to support download cancellation and lifecycle + * management. + * + * @type {import("child_process").ChildProcess|null} + */ let currentDownloadProcess = null; -// Détecte si une URL est une playlist +/** + * Determines whether a URL targets a playlist. + * + * The detection is based on the presence of the YouTube + * playlist query parameter (`list`). + * + * @param {string} url - URL to inspect. + * @returns {boolean} True if the URL appears to be a playlist. + */ function isPlaylistUrl(url) { return url.includes("?list=") || url.includes("&list="); } +/** + * Creates a dedicated download folder for a playlist. + * + * The playlist title is sanitized to ensure it can be safely + * used as a filesystem path. If the target folder already + * exists, an incremented suffix is appended until a unique + * folder name is found. + * + * If folder creation fails, a fallback folder named + * "Untitled Playlist X" is generated. + * + * @param {string} basePath - Parent download directory. + * @param {string} playlistTitle - Playlist title returned by yt-dlp. + * @returns {string} Absolute path of the created playlist folder. + * @throws {Error} If no valid folder can be created. + */ function createPlaylistFolder(basePath, playlistTitle) { const sanitizedTitle = playlistTitle .replace(/[<>:"|?*]/g, '') @@ -74,6 +106,32 @@ function createPlaylistFolder(basePath, playlistTitle) { } } +/** + * Starts a yt-dlp download process and monitors its progress. + * + * Responsibilities: + * - Validates and prepares the output directory + * - Creates playlist folders when enabled + * - Builds yt-dlp command arguments + * - Spawns the yt-dlp process + * - Parses progress, speed and stage information + * - Notifies listeners about download events + * - Handles download completion and failures + * + * @param {Object} options - Download options. + * @param {string} options.url - Video or playlist URL. + * @param {string} [options.outputFolder] - Destination folder. + * @param {string} [options.playlistTitle] - Playlist title. + * + * @param {Function[]} listeners - Progress listeners. + * @param {Function[]} speedListeners - Download speed listeners. + * @param {Function[]} stageListeners - Processing stage listeners. + * @param {Function[]} playlistInfoListeners - Playlist progress listeners. + * + * @returns {Promise} + * Resolves with the final output folder path once the + * download has completed successfully. + */ function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) { return new Promise((resolve, reject) => { @@ -191,6 +249,16 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl }); } +/** + * Cancels the currently running yt-dlp process. + * + * Attempts to terminate the entire process tree to ensure + * that yt-dlp and any spawned FFmpeg processes are stopped. + * + * @returns {boolean} + * True if a download process was cancelled, + * otherwise false. + */ function cancelDownload() { if (currentDownloadProcess) { logger.info("Cancelling download and killing all child processes..."); diff --git a/server/services/info.services.js b/server/services/info.services.js index b4c0dfe..a52d73d 100644 --- a/server/services/info.services.js +++ b/server/services/info.services.js @@ -3,8 +3,30 @@ const { userYtDlp, denoPath } = require("../helpers/path.helpers"); const getUserBrowser = require("../helpers/getBrowser.helpers"); const { logger } = require("../logger"); +/** + * Retrieves metadata for a video or playlist using yt-dlp. + * + * The command executes yt-dlp in JSON mode and returns the + * parsed response object without downloading any media. + * + * Features: + * - Supports videos and playlists + * - Imports browser cookies when available + * - Uses Deno for JavaScript extraction + * - Applies a 30-second timeout + * - Limits response size to 10 MB + * + * @param {string} url - Video or playlist URL to inspect. + * @returns {Promise} Resolves with the metadata returned by yt-dlp. + * + */ function fetchInfo(url) { - const args = [ + + /** + * Base yt-dlp arguments used to retrieve metadata + * without downloading media files. + */ + const metadataArgs = [ "--js-runtimes", `deno:${denoPath}`, "--dump-single-json", "--ignore-errors", @@ -16,9 +38,9 @@ function fetchInfo(url) { return new Promise((resolve, reject) => { - execFile(userYtDlp,[...args, url], + execFile(userYtDlp,[...metadataArgs , url], { timeout: 30_000, // 30s, if more timeout - maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko) + maxBuffer: 10 * 1024 * 1024 }, // Maximum response size: 10 MB (Node.js default is much smaller) (error, stdout, stderr) => { From 13ee4c170b807bf730b84470d369af787c018139 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sun, 31 May 2026 21:12:35 +0200 Subject: [PATCH 08/11] fix: fix width for form --- public/styles/layout/form.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/styles/layout/form.css b/public/styles/layout/form.css index 4ef5349..a6d6a75 100644 --- a/public/styles/layout/form.css +++ b/public/styles/layout/form.css @@ -40,8 +40,7 @@ form button:hover { form#downloadForm { display: flex; gap: 0.5em; - max-width: 600px; - width: 100%; + width: 350px; justify-content: center; } From b745ebd29fbc56dbbd759ecbad3ac8c6b1c7dbb3 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sun, 31 May 2026 21:45:56 +0200 Subject: [PATCH 09/11] fix: display "dev" in app version if the app is not packaged --- config.js | 2 +- public/script/appVersion.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.js b/config.js index 73e855a..56e31aa 100644 --- a/config.js +++ b/config.js @@ -80,7 +80,7 @@ module.exports = { /** * Application version from package.json */ - version: packageJson.version, + version: devMode ? `dev-${packageJson.version}` : `v${packageJson.version}` , /** * Backend Express / internal server port diff --git a/public/script/appVersion.js b/public/script/appVersion.js index 8e8f505..4b1cb18 100644 --- a/public/script/appVersion.js +++ b/public/script/appVersion.js @@ -15,7 +15,7 @@ async function versionLabel() { if (!versionBadge) return; - versionBadge.textContent = `v${appVersion}`; + versionBadge.textContent = `${appVersion}`; } versionLabel(); \ No newline at end of file From c88189b44fbe196cd6abe33371b9211c40fe288b Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:39:12 +0200 Subject: [PATCH 10/11] feat: add Devtron in dev mode remove unused settings item add setting to disable hardware acceleration (enabled by default) optimize main.js startup loading --- app/ipcHandlers.js | 3 +- config/config.default.json | 1 + main.js | 58 +++++-- package-lock.json | 320 +++++++++++++++++++++++++++++++++++++ package.json | 4 +- public/index.html | 24 +-- 6 files changed, 379 insertions(+), 31 deletions(-) diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js index dfa1114..b616e64 100644 --- a/app/ipcHandlers.js +++ b/app/ipcHandlers.js @@ -38,7 +38,8 @@ const FEATURE_WHITELIST = new Set([ "customCodec", "theme", "createPlaylistFolders", - "notifySystem" + "notifySystem", + "enableHardwareAcceleration" ]); /** diff --git a/config/config.default.json b/config/config.default.json index 7b68c4c..b807dd7 100644 --- a/config/config.default.json +++ b/config/config.default.json @@ -13,5 +13,6 @@ "outputTitleCheck": true, "downloadSystem": true, "notifySystem": true, + "enableHardwareAcceleration": true, "theme": "dark" } \ No newline at end of file diff --git a/main.js b/main.js index c5ebbc2..e4accce 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,8 @@ +/** + * Handle crash application and avoid ghost instance + */ +const { app } = require("electron"); + process.on("uncaughtException", (err) => { console.error("Uncaught exception:", err); app.quit(); @@ -8,7 +13,6 @@ process.on("unhandledRejection", (reason) => { app.quit(); }); -const { app } = require("electron"); /** * True if this is the primary instance (lock acquired successfully) @@ -24,29 +28,59 @@ if (!isPrimaryInstance) { } /** - * Load the app + * Set App Name and AppID */ -const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager"); -const path = require("path"); +app.setName("Freedom Loader"); +app.setAppUserModelId("com.masteracnolo.freedomloader"); + +/** + * Load the app dependencies for hardware choice + */ const { logger, logSessionStart, logSessionEnd } = require("./server/logger"); -const { initAutoUpdater } = require("./app/autoUpdater"); -const { startRPC, stopRPC } = require("./app/discordRPC"); - const { configFeatures } = require("./config"); + +/** + * On start, enable or disable hardWareAcceleration + */ +if (!configFeatures.enableHardwareAcceleration){ + app.disableHardwareAcceleration(); + logger.info("Disabled Hardware Acceleration") +} else { + logger.info("Enable Hardware Acceleration") +} + +/** + * In-memory snapshot of application feature flags. + * + * Note: Changes to the config file are not automatically reflected. + * A restart could be required. + */ const config = require("./config"); + +if(config.devMode){ + /** + * Start devTron extensions - @see https://github.com/electron/devtron + */ + const { devtron } = require('@electron/devtron'); + devtron.install(); + logger.info("Loaded DevTron Extension") +} + +/** + * Load the app dependencies + */ const { initThemes } = require("./app/themeManager"); +const { initAutoUpdater } = require("./app/autoUpdater"); +const { startRPC, stopRPC } = require("./app/discordRPC"); const { checkNativeDependencies } = require("./app/dependencyCheck"); const { updateYtDlp } = require("./app/ytDlpUpdater"); const { createMainWindow, getMainWindow } = require("./app/windowManager"); const { registerIpcHandlers } = require("./app/ipcHandlers"); +const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager"); const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers"); -app.setName("Freedom Loader"); -app.setAppUserModelId("com.masteracnolo.freedomloader"); -app.disableHardwareAcceleration(); - /** * If another instance want to run @@ -87,8 +121,6 @@ app.whenReady().then(async () => { setSplashProgress(3); // Almost ready await createMainWindow(); - - await new Promise(resolve => setTimeout(resolve, 2000)); closeSplashWindow(); getMainWindow().show(); diff --git a/package-lock.json b/package-lock.json index 4e54f70..531bbf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "freedom-loader", "version": "1.6.0", "dependencies": { + "@electron/devtron": "^2.1.1", "chalk": "^4.1.2", "debug": "^4.4.1", "discord-rpc": "^4.0.1", @@ -24,6 +25,15 @@ "electron-builder": "^26.8.1" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -111,6 +121,26 @@ "node": "*" } }, + "node_modules/@electron/devtron": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@electron/devtron/-/devtron-2.1.1.tgz", + "integrity": "sha512-hiNj9faRQJYVJ69TQyJ2+BWlMAc97xwuaol1XhRb2PM4FYn7ze569kOxycQodZlEq9Sim+9yu0TxFu7//Oagxg==", + "license": "MIT", + "dependencies": { + "@fontsource-variable/roboto": "^5.2.5", + "@fontsource/space-mono": "^5.2.5", + "@microlink/react-json-view": "^1.26.2", + "ag-grid-community": "^34.1.2", + "ag-grid-react": "^34.1.2", + "denque": "^2.1.0", + "lucide-react": "^0.513.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/fuses": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", @@ -511,6 +541,24 @@ "node": ">= 10.0.0" } }, + "node_modules/@fontsource-variable/roboto": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto/-/roboto-5.2.10.tgz", + "integrity": "sha512-LJ0iLg6aHbLzN515gyHzmdTqJzd9NlI95cCg1DW0F5G7KkFqRLBBKBbqEJx4nSu4aby3IKmw3ZH6Fe928IfaSQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/space-mono": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource/space-mono/-/space-mono-5.2.9.tgz", + "integrity": "sha512-b61faFOHEISQ/pD25G+cfGY9o/WW6lRv6hBQQfpWvEJ4y1V+S4gmth95EVyBE2VL3qDYHeVQ8nBzrplzdXTDDg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -705,6 +753,24 @@ "node": ">= 10.0.0" } }, + "node_modules/@microlink/react-json-view": { + "version": "1.31.20", + "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.20.tgz", + "integrity": "sha512-gNLkGvjFDeAqVGvK3H7lfoDqetn/9lW2ugiYiJhchc7jQU1ZaKsZnt97ANluXWFfd/wifoA9TrVOTsUXwXCJwA==", + "license": "MIT", + "dependencies": { + "react-base16-styling": "~0.10.0", + "react-lifecycles-compat": "~3.0.4", + "react-textarea-autosize": "~8.5.9" + }, + "engines": { + "node": ">=17" + }, + "peerDependencies": { + "react": ">= 15", + "react-dom": ">= 15" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -852,6 +918,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -956,6 +1028,35 @@ "node": ">= 0.6" } }, + "node_modules/ag-charts-types": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.3.1.tgz", + "integrity": "sha512-5216xYoawnvMXDFI6kTpPku+mH0Csiwu/FE7lsAm8Z22HEN6ciSG/V7g+IrpLWncELqksgENebCTP75PZ3CsHA==", + "license": "MIT" + }, + "node_modules/ag-grid-community": { + "version": "34.3.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.3.1.tgz", + "integrity": "sha512-PwlrPudsFOzGumphi2y9ihWeaUlIwKhOra/MXu2LjeV2U8DgLLcYS8CartE5Hszhn1poJHawwI9HWrxlKliwdw==", + "license": "MIT", + "dependencies": { + "ag-charts-types": "12.3.1" + } + }, + "node_modules/ag-grid-react": { + "version": "34.3.1", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.3.1.tgz", + "integrity": "sha512-1UTlBT+xJkjNZAuf7RxK61mgxKGTPB+6XR99oIHq7cYC89kJmLbWqhHt/1XqRWF5cAgSKk8u+HtOQaN8tAZStw==", + "license": "MIT", + "dependencies": { + "ag-grid-community": "34.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1986,6 +2087,12 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2103,6 +2210,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3566,6 +3682,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3690,6 +3812,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -3827,6 +3955,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -3874,6 +4008,18 @@ "node": ">= 12.0.0" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -3897,6 +4043,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "0.513.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", + "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/make-fetch-happen": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", @@ -4361,6 +4516,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -4705,6 +4869,17 @@ "node": ">=10" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -4819,6 +4994,91 @@ "url": "https://opencollective.com/express" } }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-base16-styling": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.10.0.tgz", + "integrity": "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==", + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.0", + "color": "^4.2.3", + "csstype": "^3.1.3", + "lodash-es": "^4.17.21" + } + }, + "node_modules/react-base16-styling/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/react-base16-styling/node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -5033,6 +5293,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5227,6 +5493,15 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -5756,6 +6031,51 @@ "punycode": "^2.1.0" } }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index 249ce17..0c4ba19 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ }, "devDependencies": { "electron": "^41.2.0", - "electron-builder": "^26.8.1" + "electron-builder": "^26.8.1", + "@electron/devtron": "^2.1.1" + }, "build": { "appId": "com.masteracnolo.freedomloader", diff --git a/public/index.html b/public/index.html index c684b4a..7e623f0 100644 --- a/public/index.html +++ b/public/index.html @@ -175,28 +175,20 @@ +
+ +
+ Enable Hardware Acceleration + Uses GPU acceleration for rendering when available.
(Be effective on next launch)
+
+
+
Developer Settings - - - -
From 81515c6ab805264709ecd89cceec9b613927e600 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:35:29 +0200 Subject: [PATCH 11/11] fix: revert version to 1.5.1 in package.json and README.md --- README.md | 3 ++- package-lock.json | 37 ++++++++++++++++++++++++++++++++++--- package.json | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0083bb5..260050b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ### **A clean, open-source multimedia downloader for Windows** -[![Release](https://img.shields.io/badge/Release-1.6.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) +[![Release](https://img.shields.io/badge/Release-1.5.1-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/) [![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/) @@ -207,6 +207,7 @@ Freedom Loader can be configured either through the settings panel in the UI or | `outputTitleCheck` | boolean | `true` | Verify output file titles | | `downloadSystem` | boolean | `true` | Enable download system | | `notifySystem` | boolean | `true` | Enable system notifications | +| `enableHardwareAcceleration` | boolean | `true` | Enable HArdware Acceleration | | `theme` | string | `dark` | Current Theme Name | ### Supported Codecs diff --git a/package-lock.json b/package-lock.json index 531bbf0..d188a2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,13 @@ { "name": "freedom-loader", - "version": "1.6.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freedom-loader", - "version": "1.6.0", + "version": "1.5.1", "dependencies": { - "@electron/devtron": "^2.1.1", "chalk": "^4.1.2", "debug": "^4.4.1", "discord-rpc": "^4.0.1", @@ -21,6 +20,7 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { + "@electron/devtron": "^2.1.1", "electron": "^41.2.0", "electron-builder": "^26.8.1" } @@ -29,6 +29,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -125,6 +126,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@electron/devtron/-/devtron-2.1.1.tgz", "integrity": "sha512-hiNj9faRQJYVJ69TQyJ2+BWlMAc97xwuaol1XhRb2PM4FYn7ze569kOxycQodZlEq9Sim+9yu0TxFu7//Oagxg==", + "dev": true, "license": "MIT", "dependencies": { "@fontsource-variable/roboto": "^5.2.5", @@ -545,6 +547,7 @@ "version": "5.2.10", "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto/-/roboto-5.2.10.tgz", "integrity": "sha512-LJ0iLg6aHbLzN515gyHzmdTqJzd9NlI95cCg1DW0F5G7KkFqRLBBKBbqEJx4nSu4aby3IKmw3ZH6Fe928IfaSQ==", + "dev": true, "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -554,6 +557,7 @@ "version": "5.2.9", "resolved": "https://registry.npmjs.org/@fontsource/space-mono/-/space-mono-5.2.9.tgz", "integrity": "sha512-b61faFOHEISQ/pD25G+cfGY9o/WW6lRv6hBQQfpWvEJ4y1V+S4gmth95EVyBE2VL3qDYHeVQ8nBzrplzdXTDDg==", + "dev": true, "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -757,6 +761,7 @@ "version": "1.31.20", "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.20.tgz", "integrity": "sha512-gNLkGvjFDeAqVGvK3H7lfoDqetn/9lW2ugiYiJhchc7jQU1ZaKsZnt97ANluXWFfd/wifoA9TrVOTsUXwXCJwA==", + "dev": true, "license": "MIT", "dependencies": { "react-base16-styling": "~0.10.0", @@ -922,6 +927,7 @@ "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -1032,12 +1038,14 @@ "version": "12.3.1", "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.3.1.tgz", "integrity": "sha512-5216xYoawnvMXDFI6kTpPku+mH0Csiwu/FE7lsAm8Z22HEN6ciSG/V7g+IrpLWncELqksgENebCTP75PZ3CsHA==", + "dev": true, "license": "MIT" }, "node_modules/ag-grid-community": { "version": "34.3.1", "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.3.1.tgz", "integrity": "sha512-PwlrPudsFOzGumphi2y9ihWeaUlIwKhOra/MXu2LjeV2U8DgLLcYS8CartE5Hszhn1poJHawwI9HWrxlKliwdw==", + "dev": true, "license": "MIT", "dependencies": { "ag-charts-types": "12.3.1" @@ -1047,6 +1055,7 @@ "version": "34.3.1", "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.3.1.tgz", "integrity": "sha512-1UTlBT+xJkjNZAuf7RxK61mgxKGTPB+6XR99oIHq7cYC89kJmLbWqhHt/1XqRWF5cAgSKk8u+HtOQaN8tAZStw==", + "dev": true, "license": "MIT", "dependencies": { "ag-grid-community": "34.3.1", @@ -2091,6 +2100,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2214,6 +2224,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -3686,6 +3697,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, "license": "MIT" }, "node_modules/is-fullwidth-code-point": { @@ -3816,6 +3828,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3959,6 +3972,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, "license": "MIT" }, "node_modules/lodash.escaperegexp": { @@ -4012,6 +4026,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4047,6 +4062,7 @@ "version": "0.513.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", + "dev": true, "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -4520,6 +4536,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4873,6 +4890,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -4998,6 +5016,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5007,6 +5026,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.10.0.tgz", "integrity": "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==", + "dev": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.0", @@ -5019,6 +5039,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1", @@ -5032,6 +5053,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "^1.0.0", @@ -5042,6 +5064,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -5054,18 +5077,21 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true, "license": "MIT" }, "node_modules/react-textarea-autosize": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.13", @@ -5297,6 +5323,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -5497,6 +5524,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" @@ -6035,6 +6063,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6049,6 +6078,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6063,6 +6093,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "dev": true, "license": "MIT", "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" diff --git a/package.json b/package.json index 0c4ba19..0367ffa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "freedom-loader", "productName": "Freedom Loader", - "version": "1.6.0", + "version": "1.5.1", "author": "MasterAcnolo", "description": "Freedom Loader", "main": "main.js",