mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Compare commits
18 Commits
1.5.0
...
e1ef7032bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ef7032bb | ||
|
|
e7333b7b6b | ||
|
|
e2b51d32d1 | ||
|
|
81515c6ab8 | ||
|
|
c88189b44f | ||
|
|
b745ebd29f | ||
|
|
13ee4c170b | ||
|
|
0218664c5a | ||
|
|
6afc5e06d6 | ||
|
|
519dcad6e6 | ||
|
|
ac94d68224 | ||
|
|
0f876fcb70 | ||
|
|
a6c11ba7c4 | ||
|
|
44132dd7b5 | ||
|
|
15ab6892b7 | ||
|
|
725b6f0ada | ||
|
|
ad0e4f858b | ||
|
|
68108e3a66 |
47
README.md
47
README.md
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
### **A clean, open-source multimedia downloader for Windows**
|
### **A clean, open-source multimedia downloader for Windows**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
||||||
@@ -26,7 +26,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
### See related repo:
|
### See related repo:
|
||||||
|
|
||||||
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||||
- **[Workshop](https://masteracnolo.github.io/Freedom-Loader-Workshop/)** - Theme creation tool and theme browser
|
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
|
|
||||||
### Configuration File Location
|
### Configuration File Location
|
||||||
```
|
```
|
||||||
C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
%APPDATA%\Freedom Loader\config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Options
|
### Available Options
|
||||||
@@ -179,11 +179,14 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -199,11 +202,14 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
|
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||||
|
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
|
||||||
|
| `theme` | string | `dark` | Current Theme Name |
|
||||||
|
|
||||||
### Supported Codecs
|
### Supported Codecs
|
||||||
|
|
||||||
@@ -423,39 +429,6 @@ ressources/
|
|||||||
|
|
||||||
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||||
|
|
||||||
### Completed
|
|
||||||
|
|
||||||
- [x] ~~Website and documentation~~
|
|
||||||
- [x] ~~Automatic YT-DLP updates~~
|
|
||||||
- [x] ~~Automatic application updates~~
|
|
||||||
- [x] ~~Enhanced download status display~~
|
|
||||||
- [x] ~~Video chapter extraction~~
|
|
||||||
- [x] ~~Custom output path selection~~
|
|
||||||
- [x] ~~Custom codec selection~~
|
|
||||||
- [x] ~~Settings Panel with toggle switch for features.~~
|
|
||||||
- [x] ~~Custom theme system (ZIP format) ~~
|
|
||||||
- [x] ~~Theme Workshop (web tool)~~
|
|
||||||
- [x] ~~Splash screen~~
|
|
||||||
- [x] ~~Download stop button~~
|
|
||||||
- [x] ~~Config saved in AppData (preserved on update)~~
|
|
||||||
|
|
||||||
### In Progress
|
|
||||||
|
|
||||||
- [ ] Additional format support (WebM, OGG, etc.)
|
|
||||||
- [ ] Chrome/Edge cookie support
|
|
||||||
- [ ] Improved multi-site support
|
|
||||||
|
|
||||||
### Planned
|
|
||||||
|
|
||||||
- [ ] Subtitle download and embedding
|
|
||||||
- [ ] Multiple language support
|
|
||||||
- [ ] Partial video downloads
|
|
||||||
- [ ] Custom file naming patterns
|
|
||||||
- [ ] Parallel downloads
|
|
||||||
- [ ] Automatic sponsor detection (SponsorBlock integration)
|
|
||||||
- [ ] Download history
|
|
||||||
- [ ] Linux version
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome and appreciated. This project benefits from community involvement.
|
Contributions are welcome and appreciated. This project benefits from community involvement.
|
||||||
|
|||||||
@@ -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 { autoUpdater } = require("electron-updater");
|
||||||
const { dialog } = require("electron");
|
const { dialog } = require("electron");
|
||||||
const { logger } = require("../server/logger");
|
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.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = 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) {
|
function initAutoUpdater(mainWindow) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when a new version is detected.
|
||||||
|
* Prompts user to install or defer update.
|
||||||
|
*/
|
||||||
autoUpdater.on("update-available", async (info) => {
|
autoUpdater.on("update-available", async (info) => {
|
||||||
logger.info(`Update available: ${info.version}`);
|
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) => {
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
||||||
mainWindow?.webContents.send("update-progress", {
|
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) => {
|
autoUpdater.on("update-downloaded", async (info) => {
|
||||||
logger.info(`Update downloaded: ${info.version}`);
|
logger.info(`Update downloaded: ${info.version}`);
|
||||||
|
|
||||||
@@ -52,6 +91,10 @@ function initAutoUpdater(mainWindow) {
|
|||||||
if (response === 0) autoUpdater.quitAndInstall();
|
if (response === 0) autoUpdater.quitAndInstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles update system errors.
|
||||||
|
* Logs failure and displays an error dialog to user.
|
||||||
|
*/
|
||||||
autoUpdater.on("error", (err) => {
|
autoUpdater.on("error", (err) => {
|
||||||
logger.error("Auto update error:", err.message);
|
logger.error("Auto update error:", err.message);
|
||||||
dialog.showErrorBox("Update Error", err.message);
|
dialog.showErrorBox("Update Error", err.message);
|
||||||
@@ -60,6 +103,10 @@ function initAutoUpdater(mainWindow) {
|
|||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update check on startup.
|
||||||
|
* Separated from init for reusability and testability.
|
||||||
|
*/
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates();
|
||||||
@@ -68,6 +115,10 @@ async function checkForUpdates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update download.
|
||||||
|
* Used when user accepts update prompt.
|
||||||
|
*/
|
||||||
async function downloadUpdate() {
|
async function downloadUpdate() {
|
||||||
try {
|
try {
|
||||||
await autoUpdater.downloadUpdate();
|
await autoUpdater.downloadUpdate();
|
||||||
@@ -76,6 +127,9 @@ async function downloadUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately quits application and installs downloaded update.
|
||||||
|
*/
|
||||||
function installUpdate() {
|
function installUpdate() {
|
||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,33 @@ const fs = require("fs");
|
|||||||
const { app, dialog } = require("electron");
|
const { app, dialog } = require("electron");
|
||||||
const { logger } = require("../server/logger");
|
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() {
|
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");
|
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 = [
|
const deps = [
|
||||||
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
|
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
|
||||||
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
|
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
|
||||||
@@ -15,9 +39,16 @@ function checkNativeDependencies() {
|
|||||||
const missing = deps.filter(d => !fs.existsSync(d.path));
|
const missing = deps.filter(d => !fs.existsSync(d.path));
|
||||||
if (missing.length === 0) return true;
|
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(", ");
|
const list = missing.map(d => d.name).join(", ");
|
||||||
logger.error(`Missing dependencies: ${list}`);
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
dialog.showErrorBox(
|
dialog.showErrorBox(
|
||||||
"Missing dependencies",
|
"Missing dependencies",
|
||||||
|
|||||||
@@ -2,12 +2,40 @@ const config = require('../config');
|
|||||||
const RPC = require("discord-rpc");
|
const RPC = require("discord-rpc");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord Application Client ID used to authenticate the RPC connection.
|
||||||
|
* Comes from the application configuration.
|
||||||
|
*/
|
||||||
const clientId = `${config.DiscordRPCID}`;
|
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" });
|
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;
|
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() {
|
function startRPC() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich Presence payload describing current application state
|
||||||
|
* shown in Discord user profile.
|
||||||
|
*/
|
||||||
rpc.on("ready", () => {
|
rpc.on("ready", () => {
|
||||||
const presence = {
|
const presence = {
|
||||||
largeImageKey: "icon",
|
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(){
|
async function stopRPC(){
|
||||||
try {
|
try {
|
||||||
if (intervalId) clearInterval(intervalId);
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures RPC connection exists before attempting cleanup.
|
||||||
|
*/
|
||||||
if (rpc && rpc.transport) {
|
if (rpc && rpc.transport) {
|
||||||
await rpc.clearActivity()
|
await rpc.clearActivity()
|
||||||
await rpc.destroy();
|
await rpc.destroy();
|
||||||
|
|||||||
@@ -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 { ipcMain, dialog, shell } = require("electron");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
@@ -8,6 +20,12 @@ const config = require("../config");
|
|||||||
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||||
const { userThemesPath } = require("../server/helpers/path.helpers");
|
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([
|
const FEATURE_WHITELIST = new Set([
|
||||||
"autoUpdate",
|
"autoUpdate",
|
||||||
"discordRPC",
|
"discordRPC",
|
||||||
@@ -20,20 +38,49 @@ const FEATURE_WHITELIST = new Set([
|
|||||||
"customCodec",
|
"customCodec",
|
||||||
"theme",
|
"theme",
|
||||||
"createPlaylistFolders",
|
"createPlaylistFolders",
|
||||||
"notifySystem"
|
"notifySystem",
|
||||||
|
"enableHardwareAcceleration"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path to the configuration file storing feature flags.
|
||||||
|
*/
|
||||||
const configFolderPath = featuresPath;
|
const configFolderPath = featuresPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory containing user-installed themes.
|
||||||
|
*/
|
||||||
const themeFolderPath = userThemesPath;
|
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) {
|
function registerIpcHandlers(getMainWindow) {
|
||||||
|
|
||||||
// Infos générales
|
/**
|
||||||
|
* Returns application version from config.
|
||||||
|
*/
|
||||||
ipcMain.handle("version", () => config.version);
|
ipcMain.handle("version", () => config.version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns runtime feature configuration object.
|
||||||
|
*/
|
||||||
ipcMain.handle("features", () => configFeatures);
|
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 () => {
|
ipcMain.handle("select-download-folder", async () => {
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
if (result.canceled) {
|
if (result.canceled) {
|
||||||
@@ -53,48 +100,108 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
|
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
|
||||||
ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
|
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) => {
|
ipcMain.on("set-progress", (_, percent) => {
|
||||||
getMainWindow()?.setProgressBar(percent / 100);
|
getMainWindow()?.setProgressBar(percent / 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TOPBAR ACTION
|
/**
|
||||||
|
* Window minimize request from renderer.
|
||||||
|
*/
|
||||||
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/unmaximize state of main window.
|
||||||
|
*/
|
||||||
ipcMain.on("window-maximize", () => {
|
ipcMain.on("window-maximize", () => {
|
||||||
const win = getMainWindow();
|
const win = getMainWindow();
|
||||||
if (!win) return;
|
if (!win) return;
|
||||||
win.isMaximized() ? win.unmaximize() : win.maximize();
|
win.isMaximized() ? win.unmaximize() : win.maximize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the main application window.
|
||||||
|
*/
|
||||||
ipcMain.on("window-close", () => getMainWindow()?.close());
|
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools in detached mode.
|
||||||
|
*/
|
||||||
ipcMain.on("open-devtools", () =>
|
ipcMain.on("open-devtools", () =>
|
||||||
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
|
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory in system file explorer.
|
||||||
|
*/
|
||||||
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens external website in default browser.
|
||||||
|
*/
|
||||||
ipcMain.on("open-website", () =>
|
ipcMain.on("open-website", () =>
|
||||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens official wiki page in external browser.
|
||||||
|
*/
|
||||||
ipcMain.on("open-wiki", () =>
|
ipcMain.on("open-wiki", () =>
|
||||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens workshop page in external browser.
|
||||||
|
*/
|
||||||
ipcMain.on("open-workshop", () =>
|
ipcMain.on("open-workshop", () =>
|
||||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration folder in system file explorer.
|
||||||
|
*/
|
||||||
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// THEME
|
/**
|
||||||
|
* Retrieves available themes from filesystem.
|
||||||
|
*/
|
||||||
ipcMain.handle("get-themes", () => getThemes());
|
ipcMain.handle("get-themes", () => getThemes());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens theme directory in file explorer.
|
||||||
|
*/
|
||||||
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
|
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk dynamically.
|
||||||
|
*/
|
||||||
ipcMain.handle("reload-themes", async () => {
|
ipcMain.handle("reload-themes", async () => {
|
||||||
return await reloadThemes();
|
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 }) => {
|
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||||
try {
|
try {
|
||||||
if (!FEATURE_WHITELIST.has(key)) {
|
if (!FEATURE_WHITELIST.has(key)) {
|
||||||
|
|||||||
@@ -2,28 +2,77 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached resolved default download path.
|
||||||
|
* Initialized lazily on first access to avoid unnecessary imports.
|
||||||
|
*/
|
||||||
let _defaultPath = null;
|
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() {
|
function getDefaultDownloadPath() {
|
||||||
|
|
||||||
if (!_defaultPath) {
|
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");
|
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
_defaultPath = defaultDownloadFolder;
|
_defaultPath = defaultDownloadFolder;
|
||||||
|
|
||||||
}
|
}
|
||||||
return _defaultPath;
|
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) {
|
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");
|
const { isSafePath } = require("../server/helpers/validation.helpers");
|
||||||
|
|
||||||
if (!userPath) return getDefaultDownloadPath();
|
if (!userPath) return getDefaultDownloadPath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||||
|
* Required to prevent path traversal or alias bypass.
|
||||||
|
*/
|
||||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
if (!isSafePath(resolved)) {
|
if (!isSafePath(resolved)) {
|
||||||
throw new Error("Path not allowed: system folders are blocked!");
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
}
|
}
|
||||||
return resolved;
|
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}`);
|
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||||
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
const { BrowserWindow, app } = require("electron");
|
const { BrowserWindow, app } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the splash screen BrowserWindow instance.
|
||||||
|
* Used during application startup before main window is ready.
|
||||||
|
*/
|
||||||
let splashWindow = null;
|
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() {
|
function createSplashWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for splash screen BrowserWindow.
|
||||||
|
* Optimized for minimal UI and fast startup display.
|
||||||
|
*/
|
||||||
const splashOptions = {
|
const splashOptions = {
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -19,13 +36,29 @@ const splashOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the splash screen window.
|
||||||
|
*/
|
||||||
splashWindow = new BrowserWindow(splashOptions);
|
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");
|
const splashPath = path.join(__dirname, "../public/splash.html");
|
||||||
splashWindow.loadFile(splashPath);
|
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', () => {
|
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;
|
let bannerPath;
|
||||||
|
|
||||||
// Check if app is packaged
|
// Check if app is packaged
|
||||||
@@ -36,12 +69,21 @@ const splashOptions = {
|
|||||||
bannerPath = path.join(__dirname, '../build/banner.png');
|
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(`
|
splashWindow.webContents.executeJavaScript(`
|
||||||
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
|
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() {
|
function closeSplashWindow() {
|
||||||
if (splashWindow) {
|
if (splashWindow) {
|
||||||
splashWindow.close();
|
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) {
|
function setSplashProgress(step) {
|
||||||
if (splashWindow) {
|
if (splashWindow) {
|
||||||
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
|
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
|
||||||
|
|||||||
@@ -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 fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
const JSZip = require("jszip");
|
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;
|
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"];
|
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"];
|
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"];
|
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 = [
|
const REQUIRED_KEYS = [
|
||||||
["meta", "name"],
|
["meta", "name"],
|
||||||
["meta", "author"],
|
["meta", "author"],
|
||||||
@@ -20,10 +54,25 @@ const REQUIRED_KEYS = [
|
|||||||
["style", "progressBar", "fill"],
|
["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) {
|
function getNestedValue(obj, keys) {
|
||||||
return keys.reduce((acc, key) => acc?.[key], obj);
|
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) {
|
function validateThemeJson(json) {
|
||||||
for (const keyPath of REQUIRED_KEYS) {
|
for (const keyPath of REQUIRED_KEYS) {
|
||||||
if (getNestedValue(json, keyPath) === undefined) {
|
if (getNestedValue(json, keyPath) === undefined) {
|
||||||
@@ -33,6 +82,14 @@ function validateThemeJson(json) {
|
|||||||
return { valid: true };
|
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) {
|
function buildThemeObject(themeId, themeJson, imageData) {
|
||||||
return {
|
return {
|
||||||
id: themeId.toLowerCase(),
|
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) {
|
function extractImage(buffer, filename) {
|
||||||
if (buffer.length > MAX_IMAGE_SIZE) {
|
if (buffer.length > MAX_IMAGE_SIZE) {
|
||||||
logger.warn(`Image too large, ignoring`);
|
logger.warn(`Image too large, ignoring`);
|
||||||
@@ -55,6 +123,20 @@ function extractImage(buffer, filename) {
|
|||||||
return `data:image/${mime};base64,${buffer.toString("base64")}`;
|
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<object|null>}
|
||||||
|
*/
|
||||||
async function loadThemeFromZip(zipPath, themeId) {
|
async function loadThemeFromZip(zipPath, themeId) {
|
||||||
try {
|
try {
|
||||||
const zipBuffer = fs.readFileSync(zipPath);
|
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<object|null>}
|
||||||
|
*/
|
||||||
async function loadThemeFromFolder(folderPath, themeId) {
|
async function loadThemeFromFolder(folderPath, themeId) {
|
||||||
try {
|
try {
|
||||||
const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
|
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;
|
let cachedThemes = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active theme directory path used as source of truth.
|
||||||
|
*/
|
||||||
let themeFolderPath = null;
|
let themeFolderPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active theme directory path.
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
*/
|
||||||
function setThemeFolderPath(folderPath) {
|
function setThemeFolderPath(folderPath) {
|
||||||
themeFolderPath = 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<object[]>} List of valid theme objects
|
||||||
|
*/
|
||||||
async function loadThemesFromFolder() {
|
async function loadThemesFromFolder() {
|
||||||
const themes = [];
|
const themes = [];
|
||||||
|
|
||||||
@@ -161,11 +277,31 @@ async function loadThemesFromFolder() {
|
|||||||
return themes;
|
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) {
|
function getThemeOrder(id) {
|
||||||
const index = THEME_ORDER.indexOf(id);
|
const index = THEME_ORDER.indexOf(id);
|
||||||
return index === -1 ? Infinity : index;
|
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) {
|
async function initThemes(folderPath) {
|
||||||
setThemeFolderPath(folderPath);
|
setThemeFolderPath(folderPath);
|
||||||
const themes = await loadThemesFromFolder();
|
const themes = await loadThemesFromFolder();
|
||||||
@@ -174,10 +310,24 @@ async function initThemes(folderPath) {
|
|||||||
logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`);
|
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() {
|
function getThemes() {
|
||||||
return cachedThemes ?? [];
|
return cachedThemes ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk and refreshes cache.
|
||||||
|
*
|
||||||
|
* Used when themes are modified at runtime without restart.
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]>}
|
||||||
|
*/
|
||||||
async function reloadThemes() {
|
async function reloadThemes() {
|
||||||
const themes = await loadThemesFromFolder();
|
const themes = await loadThemesFromFolder();
|
||||||
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Electron main window module dependencies.
|
||||||
|
* Handles BrowserWindow lifecycle and application UI shell.
|
||||||
|
*/
|
||||||
|
|
||||||
const { BrowserWindow, app } = require("electron");
|
const { BrowserWindow, app } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
const { configFeatures } = require("../config");
|
const { configFeatures } = require("../config");
|
||||||
const config = 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;
|
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<BrowserWindow>} The created or existing window instance
|
||||||
|
*/
|
||||||
async function createMainWindow() {
|
async function createMainWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents multiple instances of the main window.
|
||||||
|
* Returns existing instance if already created.
|
||||||
|
*/
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
logger.warn("Window already exists, no new creation!");
|
logger.warn("Window already exists, no new creation!");
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconPath = config.localMode
|
/**
|
||||||
|
* 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(__dirname, "../build/app-icon.ico")
|
||||||
: path.join(process.resourcesPath, "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 = {
|
const windowOptions = {
|
||||||
title: `Freedom Loader ${config.version}`,
|
title: `Freedom Loader ${config.version}`,
|
||||||
icon: iconPath,
|
icon: iconPath,
|
||||||
@@ -35,6 +69,10 @@ async function createMainWindow() {
|
|||||||
|
|
||||||
mainWindow = new BrowserWindow(windowOptions);
|
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 {
|
try {
|
||||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
logger.info("Window loaded");
|
logger.info("Window loaded");
|
||||||
@@ -43,6 +81,10 @@ async function createMainWindow() {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up window reference when the main window is closed.
|
||||||
|
* Prevents memory leaks and allows recreation if needed.
|
||||||
|
*/
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
logger.info("Main window closed");
|
logger.info("Main window closed");
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
@@ -51,6 +93,14 @@ async function createMainWindow() {
|
|||||||
return mainWindow;
|
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() {
|
function getMainWindow() {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
const { execFile } = require("child_process");
|
const { execFile } = require("child_process");
|
||||||
const { logger } = require("../server/logger");
|
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) {
|
function updateYtDlp(ytDlpPath) {
|
||||||
|
|
||||||
logger.info("yt-dlp update check starting...");
|
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) => {
|
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);
|
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
|
||||||
|
|
||||||
else logger.info(`yt-dlp update: ${stdout.trim()}`);
|
else logger.info(`yt-dlp update: ${stdout.trim()}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
82
config.js
82
config.js
@@ -3,41 +3,107 @@ const { app } = require("electron");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const localMode = !app.isPackaged;
|
/**
|
||||||
|
* 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() {
|
function resolveConfigPath() {
|
||||||
if (localMode) {
|
if (devMode) {
|
||||||
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
|
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
|
||||||
|
|
||||||
if (!fs.existsSync(devConfigPath)) {
|
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);
|
fs.copyFileSync(defaultConfigPath, devConfigPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return devConfigPath;
|
return devConfigPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userConfigPath = path.join(app.getPath("userData"), "config.json");
|
const userConfigPath = path.join(app.getPath("userData"), "config.json");
|
||||||
|
|
||||||
if (!fs.existsSync(userConfigPath)) {
|
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);
|
fs.copyFileSync(defaultConfigPath, userConfigPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return userConfigPath;
|
return userConfigPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file used at runtime.
|
||||||
|
*/
|
||||||
const featuresPath = resolveConfigPath();
|
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() {
|
function loadFeatures() {
|
||||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
return JSON.parse(raw);
|
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();
|
const configFeatures = loadFeatures();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: packageJson.version,
|
/**
|
||||||
|
* Application version from package.json
|
||||||
|
*/
|
||||||
|
version: devMode ? `dev-${packageJson.version}` : `v${packageJson.version}` ,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend Express / internal server port
|
||||||
|
*/
|
||||||
applicationPort: "8787",
|
applicationPort: "8787",
|
||||||
localMode,
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the app is running in development mode
|
||||||
|
*/
|
||||||
|
devMode,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord RPC application identifier
|
||||||
|
*/
|
||||||
DiscordRPCID: "1410934537051181146",
|
DiscordRPCID: "1410934537051181146",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime feature flags loaded from configuration file
|
||||||
|
*/
|
||||||
configFeatures,
|
configFeatures,
|
||||||
featuresPath
|
|
||||||
}
|
/**
|
||||||
|
* Path to the active configuration file
|
||||||
|
*/
|
||||||
|
featuresPath,
|
||||||
|
};
|
||||||
@@ -13,5 +13,6 @@
|
|||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true,
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
"theme": "dark"
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
94
main.js
94
main.js
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Handle crash application and avoid ghost instance
|
||||||
|
*/
|
||||||
|
const { app } = require("electron");
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
console.error("Uncaught exception:", err);
|
console.error("Uncaught exception:", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
@@ -8,44 +13,97 @@ process.on("unhandledRejection", (reason) => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { app } = require("electron");
|
|
||||||
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set App Name and AppID
|
||||||
|
*/
|
||||||
|
app.setName("Freedom Loader");
|
||||||
|
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the app dependencies for hardware choice
|
||||||
|
*/
|
||||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||||
const { initAutoUpdater } = require("./app/autoUpdater");
|
|
||||||
const { startRPC, stopRPC } = require("./app/discordRPC");
|
|
||||||
|
|
||||||
const { configFeatures } = require("./config");
|
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");
|
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 { initThemes } = require("./app/themeManager");
|
||||||
|
|
||||||
|
const { initAutoUpdater } = require("./app/autoUpdater");
|
||||||
|
const { startRPC, stopRPC } = require("./app/discordRPC");
|
||||||
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
||||||
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||||
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||||
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||||
|
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||||
const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
|
const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
|
||||||
|
|
||||||
app.setName("Freedom Loader");
|
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
|
|
||||||
if (!config.localMode) {
|
/**
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
* If another instance want to run
|
||||||
if (gotLock) {
|
*/
|
||||||
app.on("second-instance", () => {
|
app.on("second-instance", () => {
|
||||||
logger.info("New instance detected, closing older...");
|
logger.info("Second instance detected");
|
||||||
getMainWindow()?.destroy();
|
|
||||||
|
const mainWindow = require("./app/windowManager").getMainWindow();
|
||||||
|
|
||||||
|
if (!mainWindow) return;
|
||||||
|
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart();
|
||||||
|
|
||||||
createSplashWindow();
|
createSplashWindow();
|
||||||
|
|
||||||
if (!config.localMode && !checkNativeDependencies()) return;
|
if (!config.devMode && !checkNativeDependencies()) return;
|
||||||
|
|
||||||
const { userYtDlp } = require("./server/helpers/path.helpers");
|
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||||
updateYtDlp(userYtDlp);
|
updateYtDlp(userYtDlp);
|
||||||
@@ -64,8 +122,6 @@ app.whenReady().then(async () => {
|
|||||||
setSplashProgress(3); // Almost ready
|
setSplashProgress(3); // Almost ready
|
||||||
await createMainWindow();
|
await createMainWindow();
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
closeSplashWindow();
|
closeSplashWindow();
|
||||||
getMainWindow().show();
|
getMainWindow().show();
|
||||||
if (configFeatures.discordRPC) startRPC();
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|||||||
393
package-lock.json
generated
393
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
@@ -20,10 +20,21 @@
|
|||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/devtron": "^2.1.1",
|
||||||
"electron": "^41.2.0",
|
"electron": "^41.2.0",
|
||||||
"electron-builder": "^26.8.1"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||||
@@ -111,6 +122,27 @@
|
|||||||
"node": "*"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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": {
|
"node_modules/@electron/fuses": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
|
||||||
@@ -511,6 +543,26 @@
|
|||||||
"node": ">= 10.0.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -705,6 +757,25 @@
|
|||||||
"node": ">= 10.0.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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": {
|
"node_modules/@npmcli/agent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
|
||||||
@@ -852,6 +923,13 @@
|
|||||||
"@types/node": "*"
|
"@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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@@ -917,9 +995,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
|
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -956,6 +1034,38 @@
|
|||||||
"node": ">= 0.6"
|
"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==",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
@@ -1355,9 +1465,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1986,6 +2096,13 @@
|
|||||||
"node": ">= 8"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2103,6 +2220,16 @@
|
|||||||
"node": ">=0.4.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -2794,12 +2921,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "8.3.2",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
|
||||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
"integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "10.1.0"
|
"ip-address": "^10.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -3549,9 +3676,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.1.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@@ -3566,6 +3693,13 @@
|
|||||||
"node": ">= 0.10"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -3690,6 +3824,13 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
@@ -3827,6 +3968,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.escaperegexp": {
|
"node_modules/lodash.escaperegexp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
@@ -3874,6 +4022,19 @@
|
|||||||
"node": ">= 12.0.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
@@ -3897,6 +4058,16 @@
|
|||||||
"node": ">=10"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/make-fetch-happen": {
|
"node_modules/make-fetch-happen": {
|
||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz",
|
||||||
@@ -4361,6 +4532,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-hash": {
|
"node_modules/object-hash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
@@ -4705,6 +4886,18 @@
|
|||||||
"node": ">=10"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proper-lockfile": {
|
"node_modules/proper-lockfile": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||||
@@ -4752,9 +4945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -4819,6 +5012,99 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"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",
|
||||||
|
"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": {
|
"node_modules/read-binary-file-arch": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
||||||
@@ -5033,6 +5319,13 @@
|
|||||||
"node": ">=11.0.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -5227,6 +5520,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||||
@@ -5613,9 +5916,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5756,6 +6059,54 @@
|
|||||||
"punycode": "^2.1.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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": {
|
"node_modules/utf8-byte-length": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo",
|
||||||
"description": "Freedom Loader",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^41.2.0",
|
"electron": "^41.2.0",
|
||||||
"electron-builder": "^26.8.1"
|
"electron-builder": "^26.8.1",
|
||||||
|
"@electron/devtron": "^2.1.1"
|
||||||
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
|
|||||||
107
preload.js
107
preload.js
@@ -1,27 +1,124 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
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", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default system download directory.
|
||||||
|
*/
|
||||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
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"),
|
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),
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application feature flags (runtime configuration).
|
||||||
|
*/
|
||||||
getFeatures: () => ipcRenderer.invoke("features"),
|
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"),
|
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"),
|
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", {
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimizes the application window.
|
||||||
|
*/
|
||||||
minimize: () => ipcRenderer.send("window-minimize"),
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/restore state of the application window.
|
||||||
|
*/
|
||||||
maximize: () => ipcRenderer.send("window-maximize"),
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the application window.
|
||||||
|
*/
|
||||||
close: () => ipcRenderer.send("window-close"),
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools for debugging.
|
||||||
|
*/
|
||||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory or log viewer.
|
||||||
|
*/
|
||||||
openLogs: () => ipcRenderer.send("open-logs"),
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the official website or project homepage.
|
||||||
|
*/
|
||||||
openWebsite: () => ipcRenderer.send("open-website"),
|
openWebsite: () => ipcRenderer.send("open-website"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the theme directory or theme editor interface.
|
||||||
|
*/
|
||||||
openTheme: () => ipcRenderer.send("open-theme"),
|
openTheme: () => ipcRenderer.send("open-theme"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens project wiki/documentation.
|
||||||
|
*/
|
||||||
openWiki: () => ipcRenderer.send("open-wiki"),
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the workshop or external content library.
|
||||||
|
*/
|
||||||
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||||
openConfig: () => ipcRenderer.send("open-config")
|
|
||||||
|
/**
|
||||||
|
* Opens configuration/settings panel.
|
||||||
|
*/
|
||||||
|
openConfig: () => ipcRenderer.send("open-config"),
|
||||||
});
|
});
|
||||||
@@ -175,28 +175,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="enableHardwareAcceleration" id="enableHardwareAcceleration">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Enable Hardware Acceleration</span>
|
||||||
|
<small>Uses GPU acceleration for rendering when available. <br> (Be effective on next launch)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Developer Settings -->
|
<!-- Developer Settings -->
|
||||||
<details class="settings-section">
|
<details class="settings-section">
|
||||||
<summary>Developer Settings</summary>
|
<summary>Developer Settings</summary>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Download System</span>
|
|
||||||
<small>Enable/disable the internal download system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Log System</span>
|
|
||||||
<small>Enable logging system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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() {
|
async function versionLabel() {
|
||||||
const appVersion = await window.electronAPI.getVersion();
|
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 = `${appVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
versionLabel();
|
versionLabel();
|
||||||
@@ -1,35 +1,42 @@
|
|||||||
// Clipboard paste functionality for URL input
|
/**
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
* Initializes clipboard paste functionality for the URL input field.
|
||||||
const pasteBtn = document.getElementById('pasteBtn');
|
*
|
||||||
const urlInput = document.getElementById('UrlInput');
|
* 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) {
|
if (!pasteBtn || !urlInput) return;
|
||||||
pasteBtn.addEventListener('click', async () => {
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
try {
|
||||||
// Read text from clipboard
|
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
|
|
||||||
// Paste into the URL input
|
|
||||||
urlInput.value = text;
|
urlInput.value = text;
|
||||||
|
|
||||||
// Trigger input event to ensure any listeners are notified
|
// Notify frameworks / listeners bound to input changes
|
||||||
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
|
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
|
||||||
// Optional: Focus the input
|
|
||||||
urlInput.focus();
|
urlInput.focus();
|
||||||
|
|
||||||
// Visual feedback
|
// Temporary UI feedback on paste action
|
||||||
pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)';
|
pasteBtn.style.backgroundColor = "var(--form-button-bg-hover-color)";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
pasteBtn.style.backgroundColor = '';
|
pasteBtn.style.backgroundColor = "";
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to read clipboard:', err);
|
console.error("Clipboard access failed:", err);
|
||||||
|
|
||||||
// Fallback: prompt user if clipboard API fails
|
alert(
|
||||||
alert('Unable to access clipboard. Please paste manually using Ctrl+V.');
|
"Unable to access clipboard. Please paste manually using Ctrl+V."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
@@ -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 () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
const savePathElem = document.getElementById("savePath");
|
const savePathElem = document.getElementById("savePath");
|
||||||
const form = document.getElementById("downloadForm");
|
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");
|
let hidden = document.getElementById("savePathInput");
|
||||||
|
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
hidden = document.createElement("input");
|
hidden = document.createElement("input");
|
||||||
hidden.type = "hidden";
|
hidden.type = "hidden";
|
||||||
@@ -12,6 +27,17 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
form.appendChild(hidden);
|
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) {
|
async function applyPathFromBack(path) {
|
||||||
savePathElem.textContent = path;
|
savePathElem.textContent = path;
|
||||||
savePathElem.title = path;
|
savePathElem.title = path;
|
||||||
@@ -19,6 +45,14 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
localStorage.setItem("customDownloadPath", path); // UX only
|
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() {
|
async function loadInitialPath() {
|
||||||
// On affiche ce que le user a vu la dernière fois (UX)
|
// On affiche ce que le user a vu la dernière fois (UX)
|
||||||
const cached = localStorage.getItem("customDownloadPath");
|
const cached = localStorage.getItem("customDownloadPath");
|
||||||
@@ -42,13 +76,16 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
await loadInitialPath();
|
await loadInitialPath();
|
||||||
|
|
||||||
document
|
/**
|
||||||
.getElementById("changePath")
|
* Opens system folder selector and updates download path.
|
||||||
.addEventListener("click", async () => {
|
*
|
||||||
|
* The selected folder is validated by Electron backend
|
||||||
|
* before being applied to the UI and persisted.
|
||||||
|
*/
|
||||||
|
document.getElementById("changePath").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
// selectDownloadFolder already returns a validated path
|
// selectDownloadFolder already returns a validated path
|
||||||
const validatedPath =
|
const validatedPath = await window.electronAPI.selectDownloadFolder();
|
||||||
await window.electronAPI.selectDownloadFolder();
|
|
||||||
|
|
||||||
if (!validatedPath) return; // cancelled
|
if (!validatedPath) return; // cancelled
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
/**
|
||||||
|
* Currently selected themes loaded from backend.
|
||||||
|
* Used as cache for UI theme switching.
|
||||||
|
*/
|
||||||
let loadedThemes = [];
|
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) {
|
function applyTheme(theme) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const style = theme.style;
|
const style = theme.style;
|
||||||
@@ -15,72 +31,75 @@ function applyTheme(theme) {
|
|||||||
document.body.style.backgroundImage = "";
|
document.body.style.backgroundImage = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors
|
// CSS variables (design tokens)
|
||||||
root.style.setProperty("--background-color", style.colors?.background || "");
|
root.style.setProperty("--background-color", style.colors?.background || "#121212");
|
||||||
root.style.setProperty("--default-text-color", style.colors?.text?.default || "");
|
root.style.setProperty("--default-text-color", style.colors?.text?.default || "#e0e0e0");
|
||||||
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "");
|
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "#2196f3");
|
||||||
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "");
|
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "#f5f5f5");
|
||||||
|
|
||||||
// Form
|
root.style.setProperty("--form-bg-color", style.form?.background || "#1e1e1e");
|
||||||
root.style.setProperty("--form-bg-color", style.form?.background || "");
|
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "#2a2a2a");
|
||||||
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "");
|
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "#444444");
|
||||||
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "");
|
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "#2196f3");
|
||||||
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "");
|
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "#f5f5f5");
|
||||||
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "");
|
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "#888888");
|
||||||
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "");
|
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "#2196f3");
|
||||||
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "");
|
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "#ffffff");
|
||||||
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "");
|
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "#1976d2");
|
||||||
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "");
|
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "#ffffff");
|
||||||
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "");
|
|
||||||
|
|
||||||
// Checkbox
|
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "#555555");
|
||||||
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "");
|
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "#2196f3");
|
||||||
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "");
|
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "#ffffff");
|
||||||
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "");
|
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "rgba(33, 150, 243, 0.5)");
|
||||||
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "");
|
|
||||||
|
|
||||||
// Download
|
root.style.setProperty("--download-status-color", style.download?.status || "#2196f3");
|
||||||
root.style.setProperty("--download-status-color", style.download?.status || "");
|
|
||||||
|
|
||||||
// Progress bar
|
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "#444444");
|
||||||
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "");
|
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "#2196f3");
|
||||||
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "");
|
|
||||||
|
|
||||||
// Video info
|
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "#1a1a1a");
|
||||||
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "");
|
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "#e0e0e0");
|
||||||
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "");
|
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "#2196f3");
|
||||||
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "");
|
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "#cccccc");
|
||||||
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "");
|
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "#ffffff");
|
||||||
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "");
|
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "#2196f3");
|
||||||
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "");
|
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "#1976d2");
|
||||||
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "");
|
|
||||||
|
|
||||||
// Playlist
|
root.style.setProperty("--playlist-background-color", style.playlist?.background || "#303030");
|
||||||
root.style.setProperty("--playlist-background-color", style.playlist?.background || "");
|
|
||||||
|
|
||||||
// Settings
|
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "#1e1e1e");
|
||||||
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "");
|
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "#e0e0e0");
|
||||||
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "");
|
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "#2196f3");
|
||||||
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "");
|
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "#ffffff");
|
||||||
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 || "#1e1e1e");
|
||||||
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 || "#e0e0e0");
|
||||||
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 || "#1e1e1e");
|
||||||
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "");
|
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "#2a2a2a");
|
||||||
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "");
|
root.style.setProperty("--settings-text-color", style.settings?.text || "#e0e0e0");
|
||||||
root.style.setProperty("--settings-text-color", style.settings?.text || "");
|
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "#aaaaaa");
|
||||||
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "");
|
|
||||||
|
|
||||||
// Subtitle
|
|
||||||
const subtitleEl = document.getElementById("subtitle");
|
const subtitleEl = document.getElementById("subtitle");
|
||||||
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists selected theme ID into Electron backend features storage.
|
||||||
|
*
|
||||||
|
* @param {string} themeId
|
||||||
|
*/
|
||||||
function saveTheme(themeId) {
|
function saveTheme(themeId) {
|
||||||
window.electronAPI.setFeature("theme", themeId);
|
window.electronAPI.setFeature("theme", themeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates theme dropdown selector with available themes.
|
||||||
|
*
|
||||||
|
* @param {Array} themes
|
||||||
|
*/
|
||||||
function populateThemeSelect(themes) {
|
function populateThemeSelect(themes) {
|
||||||
themeSelect.innerHTML = "";
|
themeSelect.innerHTML = "";
|
||||||
|
|
||||||
for (const theme of themes) {
|
for (const theme of themes) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = theme.id;
|
option.value = theme.id;
|
||||||
@@ -89,35 +108,143 @@ function populateThemeSelect(themes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes theme system:
|
||||||
|
* - loads themes from backend
|
||||||
|
* - restores saved theme
|
||||||
|
* - applies selected theme
|
||||||
|
*/
|
||||||
async function initThemes() {
|
async function initThemes() {
|
||||||
loadedThemes = await window.electronAPI.getThemes();
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
|
|
||||||
if (!loadedThemes.length) return;
|
if (!loadedThemes.length) {
|
||||||
|
const defaultTheme = {
|
||||||
|
id: "dark",
|
||||||
|
name: "Dark",
|
||||||
|
subtitle: "Darkness is my ally",
|
||||||
|
style: {
|
||||||
|
background: {
|
||||||
|
size: "cover",
|
||||||
|
position: "center",
|
||||||
|
attachment: "fixed"
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
background: "#121212",
|
||||||
|
text: {
|
||||||
|
default: "#e0e0e0",
|
||||||
|
subtitle: "#2196f3",
|
||||||
|
audioOnly: "#f5f5f5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
form: {
|
||||||
|
background: "#1e1e1e",
|
||||||
|
input: {
|
||||||
|
background: "#2a2a2a",
|
||||||
|
border: "#444444",
|
||||||
|
borderFocus: "#2196f3",
|
||||||
|
text: "#f5f5f5",
|
||||||
|
placeholder: "#888888"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff",
|
||||||
|
hover: "#1976d2"
|
||||||
|
},
|
||||||
|
pasteButtonIcon: "#ffffff"
|
||||||
|
},
|
||||||
|
|
||||||
|
checkbox: {
|
||||||
|
background: {
|
||||||
|
unchecked: "#555555",
|
||||||
|
checked: "#2196f3"
|
||||||
|
},
|
||||||
|
checkmarkBorder: "#ffffff",
|
||||||
|
pulse: "rgba(33, 150, 243, 0.5)"
|
||||||
|
},
|
||||||
|
|
||||||
|
download: {
|
||||||
|
status: "#2196f3"
|
||||||
|
},
|
||||||
|
|
||||||
|
progressBar: {
|
||||||
|
background: "#444444",
|
||||||
|
fill: "#2196f3"
|
||||||
|
},
|
||||||
|
|
||||||
|
videoInfo: {
|
||||||
|
background: "#1a1a1a",
|
||||||
|
text: "#e0e0e0",
|
||||||
|
heading: "#2196f3",
|
||||||
|
list: "#cccccc",
|
||||||
|
strong: "#ffffff",
|
||||||
|
link: "#2196f3",
|
||||||
|
linkHover: "#1976d2"
|
||||||
|
},
|
||||||
|
|
||||||
|
playlist: {
|
||||||
|
background: "#303030"
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
button: {
|
||||||
|
background: "#1e1e1e",
|
||||||
|
text: "#e0e0e0"
|
||||||
|
},
|
||||||
|
openThemeButton: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff"
|
||||||
|
},
|
||||||
|
openJsonButton: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff"
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
modal: "#1e1e1e",
|
||||||
|
section: "#2a2a2a"
|
||||||
|
},
|
||||||
|
text: "#e0e0e0",
|
||||||
|
subtitle: "#aaaaaa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadedThemes = [defaultTheme];
|
||||||
|
}
|
||||||
|
|
||||||
populateThemeSelect(loadedThemes);
|
populateThemeSelect(loadedThemes);
|
||||||
|
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
const savedId = features.theme;
|
const savedId = features.theme;
|
||||||
|
|
||||||
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||||
|
|
||||||
themeSelect.value = theme.id;
|
themeSelect.value = theme.id;
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles theme selection change from UI dropdown.
|
||||||
|
*/
|
||||||
themeSelect.addEventListener("change", (e) => {
|
themeSelect.addEventListener("change", (e) => {
|
||||||
const theme = loadedThemes.find(t => t.id === e.target.value);
|
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||||
if (theme) {
|
if (!theme) return;
|
||||||
|
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
saveTheme(theme.id);
|
saveTheme(theme.id);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from backend and reapplies current selection.
|
||||||
|
*/
|
||||||
async function refreshThemes() {
|
async function refreshThemes() {
|
||||||
loadedThemes = await window.electronAPI.reloadThemes();
|
loadedThemes = await window.electronAPI.reloadThemes();
|
||||||
populateThemeSelect(loadedThemes);
|
populateThemeSelect(loadedThemes);
|
||||||
|
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
const savedId = features.theme;
|
const savedId = features.theme;
|
||||||
|
|
||||||
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||||
|
|
||||||
themeSelect.value = theme.id;
|
themeSelect.value = theme.id;
|
||||||
|
|||||||
@@ -2,9 +2,35 @@ const form = document.getElementById("downloadForm");
|
|||||||
const statusDiv = document.getElementById("downloadStatus");
|
const statusDiv = document.getElementById("downloadStatus");
|
||||||
const button = form.querySelector("button[type=\"submit\"]");
|
const button = form.querySelector("button[type=\"submit\"]");
|
||||||
const stopBtn = document.getElementById("stopBtn");
|
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;
|
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;
|
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) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
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() {
|
function showInitialProgress() {
|
||||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
const progressBarText = document.getElementById("downloadProgressText");
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
@@ -84,6 +117,14 @@ function showInitialProgress() {
|
|||||||
if (stopBtn) stopBtn.style.display = "inline-flex";
|
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() {
|
async function stopDownload() {
|
||||||
if (!isDownloading) return;
|
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() {
|
function resetProgressBar() {
|
||||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
if (progressWrapper) progressWrapper.style.display = "none";
|
if (progressWrapper) progressWrapper.style.display = "none";
|
||||||
|
|||||||
@@ -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) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
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) {
|
function formatSize(bytes) {
|
||||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
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<Object>} Parsed metadata or error object
|
||||||
|
*/
|
||||||
async function fetchVideoInfo(url) {
|
async function fetchVideoInfo(url) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/info", {
|
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() {
|
async function init() {
|
||||||
|
|
||||||
|
// Main input listener: triggers auto-fetch when URL changes
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const urlInput = document.getElementById("UrlInput");
|
const urlInput = document.getElementById("UrlInput");
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
|||||||
@@ -1,32 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* DOM elements used for download progress UI rendering.
|
||||||
|
* These elements are updated in real-time via SSE streams.
|
||||||
|
*/
|
||||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
const progressBar = document.getElementById("downloadProgress");
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
const progressBarText = document.getElementById("downloadProgressText")
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
|
||||||
const speedElement = document.getElementById("downloadSpeedText");
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
const stageElement = document.getElementById("downloadStage");
|
const stageElement = document.getElementById("downloadStage");
|
||||||
const playlistInfoElement = document.getElementById("playlistInfoText");
|
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 speedEvt = new EventSource("/download/speed");
|
||||||
const stageEvt = new EventSource("/download/stage");
|
const stageEvt = new EventSource("/download/stage");
|
||||||
const playlistInfoEvt = new EventSource("/download/playlist-info");
|
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() {
|
function startProgress() {
|
||||||
progressBar.style.width = "0%";
|
progressBar.style.width = "0%";
|
||||||
progressBarText.innerHTML = "0%";
|
progressBarText.innerHTML = "0%";
|
||||||
speedElement.innerHTML = "0 Mib/s";
|
speedElement.innerHTML = "0 Mib/s";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the progress UI visible and enables related controls
|
||||||
|
* such as the stop button.
|
||||||
|
*/
|
||||||
function showProgress() {
|
function showProgress() {
|
||||||
progressWrapper.style.display = "block";
|
progressWrapper.style.display = "block";
|
||||||
progressBarText.style.display = "block";
|
progressBarText.style.display = "block";
|
||||||
speedElement.style.display = "block";
|
speedElement.style.display = "block";
|
||||||
|
|
||||||
// Show stop button
|
|
||||||
const stopBtn = document.getElementById("stopBtn");
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
if (stopBtn) stopBtn.style.display = "inline-flex";
|
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) {
|
function updateProgress(percent) {
|
||||||
// Show progress div and stop button only when percent > 0
|
|
||||||
if (percent > 0 && progressWrapper.style.display !== "block") {
|
if (percent > 0 && progressWrapper.style.display !== "block") {
|
||||||
showProgress();
|
showProgress();
|
||||||
}
|
}
|
||||||
@@ -35,6 +56,10 @@ function updateProgress(percent) {
|
|||||||
progressBarText.innerHTML = `${percent}%`;
|
progressBarText.innerHTML = `${percent}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully resets the progress UI and hides all download indicators.
|
||||||
|
* Called after completion or cancellation.
|
||||||
|
*/
|
||||||
function resetProgress() {
|
function resetProgress() {
|
||||||
progressBar.style.width = "0%";
|
progressBar.style.width = "0%";
|
||||||
progressBarText.textContent = "";
|
progressBarText.textContent = "";
|
||||||
@@ -50,13 +75,19 @@ function resetProgress() {
|
|||||||
playlistInfoElement.textContent = "";
|
playlistInfoElement.textContent = "";
|
||||||
playlistInfoElement.style.display = "none";
|
playlistInfoElement.style.display = "none";
|
||||||
|
|
||||||
// Hide stop button
|
|
||||||
const stopBtn = document.getElementById("stopBtn");
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
if (stopBtn) stopBtn.style.display = "none";
|
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");
|
const evtSource = new EventSource("/download/progress");
|
||||||
|
|
||||||
evtSource.onmessage = e => {
|
evtSource.onmessage = e => {
|
||||||
if (e.data === "reset") {
|
if (e.data === "reset") {
|
||||||
startProgress();
|
startProgress();
|
||||||
@@ -65,11 +96,10 @@ evtSource.onmessage = e => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.data === "done") {
|
if (e.data === "done") {
|
||||||
// Keep progress bar visible for better UX, let toast handle the feedback
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetProgress();
|
resetProgress();
|
||||||
window.electronAPI.setProgress(-1);
|
window.electronAPI.setProgress(-1);
|
||||||
}, 2000); // Wait 2s so user sees 100% progress
|
}, 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,22 +107,30 @@ evtSource.onmessage = e => {
|
|||||||
|
|
||||||
if (!isNaN(percent)) {
|
if (!isNaN(percent)) {
|
||||||
updateProgress(percent);
|
updateProgress(percent);
|
||||||
window.electronAPI.setProgress(percent); // Update Task Bar
|
window.electronAPI.setProgress(percent);
|
||||||
// Don't hide at 100%, wait for "done" signal instead
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream: download speed updates (e.g. "5.2 MiB/s").
|
||||||
|
*/
|
||||||
speedEvt.onmessage = e => {
|
speedEvt.onmessage = e => {
|
||||||
speedElement.style.display = "block";
|
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 => {
|
stageEvt.onmessage = e => {
|
||||||
stageElement.style.display = "block";
|
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 => {
|
playlistInfoEvt.onmessage = e => {
|
||||||
playlistInfoElement.style.display = "block";
|
playlistInfoElement.style.display = "block";
|
||||||
playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15"
|
playlistInfoElement.textContent = e.data;
|
||||||
};
|
};
|
||||||
@@ -1,26 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Settings panel DOM elements used to control application feature flags
|
||||||
|
* and configuration UI interactions.
|
||||||
|
*/
|
||||||
const settingsPanel = document.getElementById("settings-panel");
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
const settingsModal = document.querySelector(".settings-modal");
|
const settingsModal = document.querySelector(".settings-modal");
|
||||||
|
|
||||||
const settingsButton = document.getElementById("settings-button");
|
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;
|
let isOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of the settings panel when clicking the settings button.
|
||||||
|
*/
|
||||||
settingsButton.addEventListener("click", () => {
|
settingsButton.addEventListener("click", () => {
|
||||||
const isOpen = settingsPanel.style.display === "block";
|
const currentlyOpen = settingsPanel.style.display === "block";
|
||||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
settingsPanel.style.display = currentlyOpen ? "none" : "block";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the settings panel and resets internal state.
|
||||||
|
*/
|
||||||
function closePanel() {
|
function closePanel() {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
settingsPanel.style.display = "none";
|
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() {
|
async function loadSettings() {
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
document
|
||||||
|
.querySelectorAll(".settings-section input, .settings-section select")
|
||||||
|
.forEach((el) => {
|
||||||
const key = el.dataset.key;
|
const key = el.dataset.key;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
if (features[key] !== undefined) {
|
if (features[key] !== undefined) {
|
||||||
if (el.type === "checkbox") el.checked = features[key];
|
if (el.type === "checkbox") el.checked = features[key];
|
||||||
else if (el.tagName === "SELECT") el.value = features[key];
|
else if (el.tagName === "SELECT") el.value = features[key];
|
||||||
@@ -28,24 +50,34 @@ async function loadSettings() {
|
|||||||
|
|
||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
if (key === "theme") return;
|
if (key === "theme") return;
|
||||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
|
||||||
|
const value = el.type === "checkbox" ? el.checked : el.value;
|
||||||
window.electronAPI.setFeature(key, value);
|
window.electronAPI.setFeature(key, value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open The JSON
|
/**
|
||||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
* Opens configuration JSON file via Electron topbar API.
|
||||||
|
*/
|
||||||
|
document.getElementById("open-json-btn")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
window.topbarAPI.openConfig();
|
window.topbarAPI.openConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open Theme Folder
|
/**
|
||||||
document.getElementById("open-theme").addEventListener("click", () => {
|
* Opens theme directory via Electron topbar API.
|
||||||
|
*/
|
||||||
|
document.getElementById("open-theme")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
window.topbarAPI.openTheme();
|
window.topbarAPI.openTheme();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh Themes
|
/**
|
||||||
document.getElementById("refresh-themes-btn").addEventListener("click", function() {
|
* Refreshes theme list and updates UI selector with a short animation.
|
||||||
|
*/
|
||||||
|
document.getElementById("refresh-themes-btn")
|
||||||
|
?.addEventListener("click", function () {
|
||||||
this.classList.add("spinning");
|
this.classList.add("spinning");
|
||||||
window.refreshThemes();
|
window.refreshThemes();
|
||||||
|
|
||||||
@@ -54,13 +86,20 @@ document.getElementById("refresh-themes-btn").addEventListener("click", function
|
|||||||
}, 600);
|
}, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicking the overlay outside the modal closes the settings panel.
|
||||||
|
*/
|
||||||
|
settingsPanel?.addEventListener("click", closePanel);
|
||||||
|
|
||||||
/* clic sur l'overlay = fermer */
|
/**
|
||||||
settingsPanel.addEventListener("click", closePanel);
|
* Prevents modal clicks from closing the settings panel
|
||||||
|
* by stopping event propagation to the overlay.
|
||||||
/* clic dans la modal = stop propagation */
|
*/
|
||||||
settingsModal.addEventListener("click", (e) => {
|
settingsModal?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes settings UI by loading backend feature flags.
|
||||||
|
*/
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
window.showToast = function(message, type = 'info', duration = 4000) {
|
/**
|
||||||
const container = document.getElementById('toast-container');
|
* Displays a toast notification in the global UI container.
|
||||||
if (!container) return;
|
*
|
||||||
|
* 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");
|
||||||
const toast = document.createElement('div');
|
toast.classList.add("toast", type);
|
||||||
toast.classList.add('toast', type);
|
|
||||||
|
|
||||||
// Icons for different types
|
|
||||||
const icons = {
|
const icons = {
|
||||||
success: '✓',
|
success: "✓",
|
||||||
error: '✕',
|
error: "✕",
|
||||||
warning: '⚠',
|
warning: "⚠",
|
||||||
info: 'ℹ'
|
info: "ℹ"
|
||||||
};
|
};
|
||||||
|
|
||||||
const icon = icons[type] || icons.info;
|
const icon = icons[type] || icons.info;
|
||||||
@@ -22,14 +31,11 @@ window.showToast = function(message, type = 'info', duration = 4000) {
|
|||||||
<button class="toast-close" title="Close">×</button>
|
<button class="toast-close" title="Close">×</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add close functionality
|
const closeBtn = toast.querySelector(".toast-close");
|
||||||
const closeBtn = toast.querySelector('.toast-close');
|
closeBtn.addEventListener("click", () => removeToast(toast));
|
||||||
closeBtn.addEventListener('click', () => removeToast(toast));
|
|
||||||
|
|
||||||
// Add to container
|
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
|
|
||||||
// Auto remove after duration
|
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
setTimeout(() => removeToast(toast), duration);
|
setTimeout(() => removeToast(toast), duration);
|
||||||
}
|
}
|
||||||
@@ -37,18 +43,41 @@ window.showToast = function(message, type = 'info', duration = 4000) {
|
|||||||
return toast;
|
return toast;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a toast element with a small exit animation.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} toast
|
||||||
|
*/
|
||||||
function removeToast(toast) {
|
function removeToast(toast) {
|
||||||
if (!toast.parentElement) return;
|
if (!toast?.parentElement) return;
|
||||||
|
|
||||||
|
toast.classList.add("removing");
|
||||||
|
|
||||||
toast.classList.add('removing');
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (toast.parentElement) {
|
|
||||||
toast.remove();
|
toast.remove();
|
||||||
}
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.showSuccess = (message, duration = 4000) => window.showToast(message, 'success', duration);
|
/**
|
||||||
window.showError = (message, duration = 5000) => window.showToast(message, 'error', duration);
|
* Shortcut: success toast
|
||||||
window.showWarning = (message, duration = 4000) => window.showToast(message, 'warning', duration);
|
*/
|
||||||
window.showInfo = (message, duration = 4000) => window.showToast(message, 'info', duration);
|
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);
|
||||||
@@ -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() {
|
function setupTopbarListeners() {
|
||||||
const { topbarAPI } = window;
|
const { topbarAPI } = window;
|
||||||
if (!topbarAPI) return;
|
if (!topbarAPI) return;
|
||||||
|
|
||||||
const minBtn = document.getElementById('minimize-btn');
|
const minBtn = document.getElementById("minimize-btn");
|
||||||
const maxBtn = document.getElementById('maximize-btn');
|
const maxBtn = document.getElementById("maximize-btn");
|
||||||
const closeBtn = document.getElementById('close-btn');
|
const closeBtn = document.getElementById("close-btn");
|
||||||
const devtoolsBtn = document.getElementById('devtools-btn');
|
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||||
const logsBtn = document.getElementById('logs-btn');
|
const logsBtn = document.getElementById("logs-btn");
|
||||||
const websiteBtn = document.getElementById('website-btn');
|
const websiteBtn = document.getElementById("website-btn");
|
||||||
const wikiBtn = document.getElementById('wiki-btn');
|
const wikiBtn = document.getElementById("wiki-btn");
|
||||||
const workshopBtn = document.getElementById('workshop-btn');
|
const workshopBtn = document.getElementById("workshop-btn");
|
||||||
|
|
||||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||||
@@ -22,12 +28,30 @@ function setupTopbarListeners() {
|
|||||||
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
|
/**
|
||||||
|
* Registers topbar event listeners after DOM is fully loaded.
|
||||||
|
*/
|
||||||
|
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
if (!features.customTopBar) {
|
if (!features.customTopBar) {
|
||||||
document.getElementById("topbar").style.display = "none";
|
const topbar = document.getElementById("topbar");
|
||||||
document.getElementById("container").style.marginTop = "0";
|
const container = document.getElementById("container");
|
||||||
document.getElementById("theme-switcher").style.top = "30px";
|
const themeSwitcher = document.getElementById("theme-switcher");
|
||||||
|
|
||||||
|
if (topbar) topbar.style.display = "none";
|
||||||
|
if (container) container.style.marginTop = "0";
|
||||||
|
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
@@ -40,8 +40,7 @@ form button:hover {
|
|||||||
form#downloadForm {
|
form#downloadForm {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
max-width: 600px;
|
width: 350px;
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,47 @@ const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
|||||||
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
||||||
const { configFeatures } = require("../../config");
|
const { configFeatures } = require("../../config");
|
||||||
|
|
||||||
const listeners = [];
|
/**
|
||||||
const speedListeners = [];
|
* Active Server-Sent Event subscribers receiving
|
||||||
const stageListeners = [];
|
* download progress updates.
|
||||||
const playlistInfoListeners = [];
|
*
|
||||||
|
* 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<void>}
|
||||||
|
*/
|
||||||
async function downloadController(req, res) {
|
async function downloadController(req, res) {
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -25,17 +61,30 @@ async function downloadController(req, res) {
|
|||||||
return res.status(400).send("Save Path Not Allowed.");
|
return res.status(400).send("Save Path Not Allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get output folder when the download is finished
|
const outputFolder = await fetchDownload(
|
||||||
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
|
options,
|
||||||
|
progressListeners,
|
||||||
|
speedListeners,
|
||||||
|
stageListeners,
|
||||||
|
playlistListeners
|
||||||
|
);
|
||||||
|
|
||||||
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
||||||
res.send("Download Done !");
|
res.send("Download Done !");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Server Error in /download : ${err.message}`);
|
logger.error(`Server Error in /download: ${err?.message ?? err}`);
|
||||||
res.status(500).send(`Server Error`);
|
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) {
|
function progressController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -44,14 +93,21 @@ function progressController(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
||||||
listeners.push(sendProgress);
|
progressListeners.push(sendProgress);
|
||||||
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
listeners.splice(listeners.indexOf(sendProgress), 1);
|
progressListeners.splice(progressListeners.indexOf(sendProgress), 1);
|
||||||
res.end();
|
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) {
|
function speedController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'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) {
|
function stageController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'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) {
|
function cancelDownloadController(req, res) {
|
||||||
const cancelled = cancelDownload();
|
const cancelled = cancelDownload();
|
||||||
if (cancelled) {
|
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) {
|
function playlistInfoController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -103,10 +187,10 @@ function playlistInfoController(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
|
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
|
||||||
playlistInfoListeners.push(sendPlaylistInfo);
|
playlistListeners.push(sendPlaylistInfo);
|
||||||
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1);
|
playlistListeners.splice(playlistListeners.indexOf(sendPlaylistInfo), 1);
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,70 +3,114 @@ const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
|||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl } = require("../helpers/validation.helpers");
|
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
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
|
* Basic validation:
|
||||||
|
* - URL must exist
|
||||||
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
|
* - Must be a string
|
||||||
|
* - Must pass custom URL validation
|
||||||
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
*/
|
||||||
|
if (!url || typeof url !== "string" || !isValidUrl(url)) {
|
||||||
if (url.includes("&list") || url.includes("?list")) {
|
return res.status(400).send("Invalid URL Or Missing");
|
||||||
logger.info("Estimated Data Type: Playlist")
|
|
||||||
|
|
||||||
} else{
|
|
||||||
logger.info("Estimated Data Type: Video")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
let data;
|
let data;
|
||||||
let fetchError = null;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary metadata fetch attempt
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
data = await fetchInfo(url);
|
data = await fetchInfo(url);
|
||||||
} catch (err) {
|
} 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"))) {
|
* Fallback strategy:
|
||||||
logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`);
|
* If playlist extraction fails, retry as single video
|
||||||
|
*/
|
||||||
|
if (isPlaylistUrl) {
|
||||||
|
|
||||||
// Remove the &list parameter
|
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];
|
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = await fetchInfo(singleVideoUrl);
|
data = await fetchInfo(singleVideoUrl);
|
||||||
logger.info(`Single video fetch succeeded after playlist failure`);
|
logger.info("Single video fetch succeeded after playlist failure");
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
logger.error(`Single video fetch also failed: ${retryErr.message}`);
|
logger.error(
|
||||||
throw err; // Throw the original error
|
`Single video fetch also failed: ${retryErr.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Preserve original error context
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize response based on yt-dlp output type
|
||||||
|
*/
|
||||||
if (data._type === "playlist") {
|
if (data._type === "playlist") {
|
||||||
|
|
||||||
const playlist = parsePlaylist(data);
|
const playlist = parsePlaylist(data);
|
||||||
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
|
|
||||||
|
logger.info(
|
||||||
|
`Playlist fetched: ${playlist.title} (${playlist.count} videos)`
|
||||||
|
);
|
||||||
|
|
||||||
return res.json(playlist);
|
return res.json(playlist);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
const video = parseVideo(data);
|
const video = parseVideo(data);
|
||||||
logger.info(`Unique Video: ${video.title}`);
|
|
||||||
|
logger.info(`Video fetched: ${video.title}`);
|
||||||
|
|
||||||
return res.json(video);
|
return res.json(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
} 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 };
|
module.exports = { infoController };
|
||||||
@@ -5,6 +5,26 @@ const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
|||||||
const { configFeatures } = require("../../config.js");
|
const { configFeatures } = require("../../config.js");
|
||||||
const { logger } = require("../logger.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){
|
function validateCodec(codec){
|
||||||
|
|
||||||
const validCodec = [
|
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 }) {
|
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||||
|
|
||||||
logger.info("--- CONFIGURATION ---");
|
logger.info("--- CONFIGURATION ---");
|
||||||
|
|||||||
@@ -4,10 +4,33 @@ const path = require("path");
|
|||||||
const notify = require("./notify.helpers")
|
const notify = require("./notify.helpers")
|
||||||
const { logger } = require("../logger");
|
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() {
|
function getUserBrowser() {
|
||||||
const userProfile = os.homedir();
|
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 = [
|
const browsers = [
|
||||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
// { 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") }
|
// { 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) {
|
for (const browser of browsers) {
|
||||||
if (fs.existsSync(browser.path)) {
|
if (fs.existsSync(browser.path)) {
|
||||||
logger.info(`Browser found: ${browser.name}`);
|
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");
|
logger.warn("No supported browser found on the system");
|
||||||
notify.notifyFirefoxBrowserMissing();
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
|
||||||
// Fallback: return "Firefox" to let YT-DLP manage error
|
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||||
// this avoid app crash
|
// This prevents the application from crashing
|
||||||
return "firefox";
|
return "firefox";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
const { Notification, shell } = require("electron");
|
const { Notification, shell } = require("electron");
|
||||||
const { iconPaths } = require("./path.helpers");
|
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) {
|
function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||||
if (!notifyEnabled) return;
|
if (!notifyEnabled) return;
|
||||||
|
if (!folder) return;
|
||||||
|
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Freedom Loader",
|
title: "Freedom Loader",
|
||||||
@@ -14,6 +25,15 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
|
|||||||
notif.show();
|
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(){
|
function notifyCookiesBrowserError(){
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Cookies Error",
|
title: "Cookies Error",
|
||||||
@@ -21,10 +41,23 @@ function notifyCookiesBrowserError(){
|
|||||||
icon: iconPaths.error,
|
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();
|
notif.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
function notifyFirefoxBrowserMissing() {
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Firefox Missing",
|
title: "Firefox Missing",
|
||||||
@@ -32,12 +65,17 @@ function notifyFirefoxBrowserMissing(){
|
|||||||
icon: iconPaths.error,
|
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();
|
notif.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
notifyDownloadFinished,
|
notifyDownloadFinished,
|
||||||
notifyCookiesBrowserError,
|
notifyCookiesBrowserError,
|
||||||
notifyFirefoxBrowserMissing
|
notifyFirefoxBrowserMissing,
|
||||||
};
|
};
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a video payload by attaching the resource type.
|
||||||
|
*
|
||||||
|
* @param {Object} data - Raw video metadata.
|
||||||
|
* @returns {Object} Normalized video object.
|
||||||
|
*/
|
||||||
function parseVideo(data) {
|
function parseVideo(data) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "video",
|
type: "video",
|
||||||
...data
|
...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) {
|
function parsePlaylist(data) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "playlist",
|
type: "playlist",
|
||||||
title: data.title || data.id,
|
title: data.title || data.id,
|
||||||
|
|||||||
@@ -7,21 +7,31 @@ const config = require("../../config.js");
|
|||||||
const { logger } = require("../logger.js");
|
const { logger } = require("../logger.js");
|
||||||
|
|
||||||
// Centralized resource paths
|
// Centralized resource paths
|
||||||
const resourcesPath = config.localMode
|
const resourcesPath = config.devMode
|
||||||
? path.join(__dirname, "../../ressources")
|
? path.join(__dirname, "../../ressources")
|
||||||
: process.resourcesPath;
|
: process.resourcesPath;
|
||||||
|
|
||||||
// Default download folder (centralized)
|
// Default download folder (centralized)
|
||||||
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
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 userYtDlp;
|
||||||
let ffmpegPath;
|
let ffmpegPath;
|
||||||
let denoPath;
|
let denoPath;
|
||||||
|
|
||||||
|
/** Source binary bundled inside application resources (used for first-time copy) */
|
||||||
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
|
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
|
||||||
|
|
||||||
if (config.localMode) {
|
/**
|
||||||
|
* 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");
|
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||||
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
||||||
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||||
@@ -35,14 +45,20 @@ if (config.localMode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification icon paths
|
/**
|
||||||
|
* Centralized notification and UI icon paths.
|
||||||
|
* Used by Electron notifications and UI components.
|
||||||
|
*/
|
||||||
const iconPaths = {
|
const iconPaths = {
|
||||||
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
||||||
error: path.join(resourcesPath, "error.png"),
|
error: path.join(resourcesPath, "error.png"),
|
||||||
app: path.join(resourcesPath, "app-icon.ico")
|
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 = {
|
const binaryPaths = {
|
||||||
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
||||||
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
||||||
@@ -50,10 +66,21 @@ const binaryPaths = {
|
|||||||
deno: path.join(resourcesPath, "binaries", "deno.exe")
|
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 userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||||
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
|
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() {
|
function initUserThemes() {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(userThemesPath)) {
|
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) {
|
function copyDirectory(src, dest) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(dest)) {
|
if (!fs.existsSync(dest)) {
|
||||||
@@ -116,8 +150,9 @@ function copyDirectory(src, dest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
// Runtime validation of critical binaries
|
||||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
if (!userYtDlp){ logger.error("Missing yt-dlp binary")}
|
||||||
if (!denoPath){ logger.error("Missing DENO")}
|
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 };
|
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
const RateLimit = require("express-rate-limit");
|
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
|
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.",
|
message: "Too many requests, please try again later.",
|
||||||
statusCode: 429, // HTTP status code for "Too Many Requests"
|
statusCode: 429,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rateLimite
|
rateLimit
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
function isValidUrl(url) {
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
@@ -8,13 +17,26 @@ function isValidUrl(url) {
|
|||||||
} catch {
|
} catch {
|
||||||
return false;
|
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) {
|
function isSafePath(folder) {
|
||||||
if (!folder || folder.length < 3) return false;
|
if (!folder || folder.length < 3) return false;
|
||||||
|
|
||||||
const unsafe = ["System32", "\\Windows"];
|
const unsafe = ["System32", "\\Windows"];
|
||||||
const resolved = path.resolve(folder);
|
const resolved = path.resolve(folder);
|
||||||
|
|
||||||
return !unsafe.some(u => resolved.includes(u));
|
return !unsafe.some(u => resolved.includes(u));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ const logFormat = format.combine(
|
|||||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
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({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
format: logFormat,
|
format: logFormat,
|
||||||
@@ -40,11 +48,17 @@ const logger = createLogger({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Log Session
|
||||||
|
*/
|
||||||
function logSessionStart() {
|
function logSessionStart() {
|
||||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||||
logger.info(`Application Version: ${config.version}`)
|
logger.info(`Application Version: ${config.version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop Log Session
|
||||||
|
*/
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const express = require("express");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { logger, logSessionEnd } = require("./logger");
|
const { logger, logSessionEnd } = require("./logger");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const { rateLimite } = require("./helpers/rateLimit.helpers");
|
const { rateLimit } = require("./helpers/rateLimit.helpers");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -14,10 +14,18 @@ app.use(express.static(path.join(__dirname, "../public")));
|
|||||||
// Routes
|
// Routes
|
||||||
app.use("/download", require("./routes/download.route"));
|
app.use("/download", require("./routes/download.route"));
|
||||||
app.use("/info", require("./routes/info.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"));
|
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() {
|
async function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = app.listen(config.applicationPort, () => {
|
const server = app.listen(config.applicationPort, () => {
|
||||||
@@ -30,6 +38,12 @@ async function startServer() {
|
|||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean exit function
|
||||||
|
* - Stop Log
|
||||||
|
* - Stop Express Server
|
||||||
|
* - Stop Electron App
|
||||||
|
*/
|
||||||
const gracefulExit = () => {
|
const gracefulExit = () => {
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
|
|||||||
@@ -8,13 +8,45 @@ const path = require("path");
|
|||||||
const { isSafePath } = require("../helpers/validation.helpers");
|
const { isSafePath } = require("../helpers/validation.helpers");
|
||||||
const { configFeatures } = require("../../config.js");
|
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;
|
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) {
|
function isPlaylistUrl(url) {
|
||||||
return url.includes("?list=") || url.includes("&list=");
|
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) {
|
function createPlaylistFolder(basePath, playlistTitle) {
|
||||||
const sanitizedTitle = playlistTitle
|
const sanitizedTitle = playlistTitle
|
||||||
.replace(/[<>:"|?*]/g, '')
|
.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<string>}
|
||||||
|
* Resolves with the final output folder path once the
|
||||||
|
* download has completed successfully.
|
||||||
|
*/
|
||||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
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() {
|
function cancelDownload() {
|
||||||
if (currentDownloadProcess) {
|
if (currentDownloadProcess) {
|
||||||
logger.info("Cancelling download and killing all child processes...");
|
logger.info("Cancelling download and killing all child processes...");
|
||||||
|
|||||||
@@ -3,8 +3,30 @@ const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
|||||||
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||||
const { logger } = require("../logger");
|
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<Object>} Resolves with the metadata returned by yt-dlp.
|
||||||
|
*
|
||||||
|
*/
|
||||||
function fetchInfo(url) {
|
function fetchInfo(url) {
|
||||||
const args = [
|
|
||||||
|
/**
|
||||||
|
* Base yt-dlp arguments used to retrieve metadata
|
||||||
|
* without downloading media files.
|
||||||
|
*/
|
||||||
|
const metadataArgs = [
|
||||||
"--js-runtimes", `deno:${denoPath}`,
|
"--js-runtimes", `deno:${denoPath}`,
|
||||||
"--dump-single-json",
|
"--dump-single-json",
|
||||||
"--ignore-errors",
|
"--ignore-errors",
|
||||||
@@ -16,9 +38,9 @@ function fetchInfo(url) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
execFile(userYtDlp,[...args, url],
|
execFile(userYtDlp,[...metadataArgs , url],
|
||||||
{ timeout: 30_000, // 30s, if more timeout
|
{ 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) => {
|
(error, stdout, stderr) => {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user