49 Commits
1.4.7 ... 1.5.0

Author SHA1 Message Date
MasterAcnolo
eff2affd42 1.5.0 !!!!!!
IT'S TIME
2026-04-14 11:20:48 +02:00
MasterAcnolo
87cf05643f Feat: Update README with detailed project structure and architecture overview 2026-04-14 11:04:55 +02:00
MasterAcnolo
ce14b8abd7 Fix: Remove emoji from error messages in download and info controllers 2026-04-14 09:31:23 +02:00
MasterAcnolo
44ed1bbf0e Feat: Add link to the most recent roadmap in README 2026-04-14 09:25:59 +02:00
MasterAcnolo
e0d017d9fd Fix: Remove z-index from container styles and add z-index to settings button 2026-04-14 09:22:31 +02:00
MasterAcnolo
c3ea1ac0bd Feat: Add section for submitting community themes in README 2026-04-14 09:22:24 +02:00
MasterAcnolo
238d4ea717 Fix: Save Themes inside "AppData" 2026-04-13 21:49:19 +02:00
MasterAcnolo
d879a5eb1d Feat: Add dynamic icon path for main window based on local mode 2026-04-13 20:57:56 +02:00
MasterAcnolo
40bea35286 Feat: Implement notification feature for download completion 2026-04-13 20:39:23 +02:00
MasterAcnolo
83ac76381a Fix: Update links to Freedom-Loader-Site in documentation and IPC handlers 2026-04-13 20:20:47 +02:00
MasterAcnolo
6ff0c71be7 Feat: Add related repository links for website and workshop in README 2026-04-13 19:48:09 +02:00
MasterAcnolo
fd0b339bcc Fix: Update link to Freedom Loader Workshop in README 2026-04-13 19:15:33 +02:00
MasterAcnolo
bc5faf38b6 Fix: Correct typo in Workshop badge link in README 2026-04-13 19:14:35 +02:00
MasterAcnolo
e744e55475 Feat: Add Theme Workshop section to README and update features list 2026-04-13 19:13:04 +02:00
MasterAcnolo
a581c9309d Fix: Update external links for website and wiki in IPC handlers 2026-04-13 19:13:00 +02:00
MasterAcnolo
61040769ee Feat: Add "Theme Workshop" button and associated IPC handler for workshop access 2026-04-12 21:40:13 +02:00
MasterAcnolo
06f6a30797 Feat: Enhance theme settings with new button styles for open theme and JSON options 2026-04-12 19:32:05 +02:00
MasterAcnolo
a2653eef57 Feat: Implement playlist download count tracking during downloads with UI updates 2026-04-12 18:24:19 +02:00
MasterAcnolo
89573d11f7 Fix: Adjust z-index for reference 2026-04-12 18:17:43 +02:00
MasterAcnolo
f0c28a7f5e Fix: Moving Custom Top BAr Settings into advanced 2026-04-12 18:15:19 +02:00
MasterAcnolo
f3c37c2498 Feat: Add createPlaylistFolders feature to organize downloads into dedicated folders with toggle settings 2026-04-12 18:13:38 +02:00
MasterAcnolo
8fe4e7c38a Fix: Update save path display to include title attribute for better accessibility 2026-04-12 17:40:01 +02:00
MasterAcnolo
912f4042e5 Fix: Enhance error handling for playlist fetch failures by retrying with single video 2026-04-12 17:34:17 +02:00
MasterAcnolo
5dc61b4a15 Fix: Adjust stop button hover scale effect for better user experience 2026-04-12 17:28:13 +02:00
MasterAcnolo
5fb49b0c9d Fix: Update button text 2026-04-12 17:04:20 +02:00
MasterAcnolo
75fdf91fcd Refactor: auto-update functionality with user prompts and download progress 2026-04-12 16:57:39 +02:00
MasterAcnolo
011e0a703a Feat: Enhance download experience with progress indicators and toast notifications 2026-04-12 12:04:05 +02:00
MasterAcnolo
5aebc78d44 Fix: IPC button not working 2026-04-12 11:22:31 +02:00
MasterAcnolo
60137a15dc Feat: Implement download cancellation feature with stop button and progress handling 2026-04-12 11:22:18 +02:00
MasterAcnolo
d05b9e2511 Feat: Add theme reloading functionality with refresh button in settings panel 2026-04-12 09:32:15 +02:00
MasterAcnolo
ebb48f4f8b Fix: Remove themes from the theme order array 2026-04-11 22:38:25 +02:00
MasterAcnolo
8be47b9821 Feat: Implement splash screen with progress indicators during startup 2026-04-11 22:36:42 +02:00
MasterAcnolo
ca8b107f61 Fix: Display custom these with a basic order 2026-04-11 22:16:05 +02:00
MasterAcnolo
f25d07cb83 Fix: Remove empty title tag from index.html and unused app styles from styles.css 2026-04-11 22:15:49 +02:00
MasterAcnolo
49e1e1b507 Feat: Add theme folder access functionality in UI 2026-04-11 18:25:17 +02:00
MasterAcnolo
23d11d8933 Feat: Custom Theme features and clean the old one 2026-04-11 18:10:51 +02:00
MasterAcnolo
9f88fe19c3 Fix: Theme Saving 2026-04-11 16:37:35 +02:00
MasterAcnolo
9b4b0306c7 fix: move config to AppData to prevent settings loss on update, add config.dev.json for dev environment 2026-04-11 16:33:58 +02:00
MasterAcnolo
96401a6e1f Refactor: split main.js into app/ modules 2026-04-11 16:14:21 +02:00
MasterAcnolo
0c7103c5c9 Update: Readme version and file structure 2026-04-11 16:13:36 +02:00
MasterAcnolo
798084e3bf Fix: Remove unusued element 2026-04-11 16:00:51 +02:00
MasterAcnolo
5c4e6669fe Fix: Update theme JSON structure for audio and button settings 2026-04-11 15:18:11 +02:00
MasterAcnolo
00c542925c Refactor: Update CSS variables
Fix Progress bar
2026-04-11 15:17:05 +02:00
MasterAcnolo
24460cf0b5 Refactor: Restructure HTML layout and enhance CSS styles for improved UI 2026-04-11 14:20:05 +02:00
MasterAcnolo
a3c1b1d753 Fix: Settings Secondary Color in LIght THeme 2026-04-11 14:01:39 +02:00
MasterAcnolo
5ade3fe144 Merge pull request #39 from MasterAcnolo/1.5.0
1.4.7
2026-04-09 13:46:20 +02:00
MasterAcnolo
577069b471 Fix: Remove unusued pictures 2026-04-09 13:35:04 +02:00
MasterAcnolo
a4da47647a Update: App Version in ReadME 2026-04-09 13:31:58 +02:00
MasterAcnolo
c5b29a0647 Update: Dependencies 2026-04-08 15:04:56 +02:00
67 changed files with 2296 additions and 1291 deletions

View File

@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
## Pre-submission Checklist
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
- [ ] I have searched existing issues
## Question

4
.gitignore vendored
View File

@@ -7,5 +7,9 @@
/ressources/ffprobe.exe
/ressources/deno.exe
/theme/**
config/config.dev.json
logs/*.json
logs/*.log

View File

@@ -148,8 +148,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
## Questions?
- Check the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
- Review the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
---

145
README.md
View File

@@ -1,16 +1,17 @@
<div align="center">
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
src="./build/banner.png"
alt="Banner"
style="width: 50%;"/></a>
### **A clean, open-source multimedia downloader for Windows**
[![Release](https://img.shields.io/badge/Release-1.4.6-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![Release](https://img.shields.io/badge/Release-1.5.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0)
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/FreedomLoader/)
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/)
[![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
</div>
@@ -22,6 +23,11 @@ Freedom Loader is a desktop application built with Electron that provides a stra
The primary goal is to make media downloading accessible to users who want offline access to their favorite content, particularly in situations where internet connectivity is unreliable or unavailable.
### See related repo:
- **[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
## Table of Contents
- [Features](#features)
@@ -30,10 +36,12 @@ The primary goal is to make media downloading accessible to users who want offli
- [Preview](#preview)
- [Configuration](#configuration)
- [Project Structure](#project-structure)
- [Theme Workshop](#Theme-Workshop)
- [Technology Stack](#technology-stack)
- [Development](#development)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Submitting a community theme](#Submitting-a-community-theme)
- [Support](#support)
- [License](#license)
@@ -53,7 +61,9 @@ The primary goal is to make media downloading accessible to users who want offli
### User Interface
- **Custom window controls** - Frameless window with custom top bar (optional)
- **Theme system** - Multiple pre-built themes with dynamic switching
- **Custom themes** — ZIP-based theme system with live preview
- **Theme Workshop** — web-based theme creator and browser
- **Toast notifications** — in-app feedback system
- **Real-time progress tracking** - Live download progress bar with network speed indicator
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
- **Settings panel** - In-app configuration interface with live updates
@@ -70,6 +80,9 @@ The primary goal is to make media downloading accessible to users who want offli
- **Rate limiting** - Express-based request throttling to prevent server overload
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
- **System notifications** - Windows notifications on download completion (clickable to open folder)
- **Splash screen** — animated loading screen on startup
- **Stop download** — ability to cancel in-progress downloads
- **Playlist folders** — dedicated folder creation per playlist
## Installation
@@ -209,19 +222,88 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
```
Freedom-Loader/
├── app/ # Electron main process modules
│ ├── autoUpdater.js
│ ├── dependencyCheck.js
│ ├── discordRPC.js
│ ├── ipcHandlers.js
│ ├── pathValidator.js
│ ├── splashManager.js
│ ├── themeManager.js
│ ├── windowManager.js
│ └── ytDlpUpdater.js
├── build/ # Build resources and assets
├── config/ # Configuration files
├── public/ # Frontend assets (HTML, CSS, JavaScript)
├── ressources/ # Internal resources (icons, binaries)
├── server/ # Express server code
│ ├── routes/ # API route handlers
│ ├── services/ # Business logic
── utils/ # Server utilities
├── .github/ # GitHub configuration and workflows
├── main.js # Electron main process
├── preload.js # Electron preload script
├── config.js # Global application configuration
└── package.json # Dependencies and npm scripts
│ ├── config.default.json
│ └── config.dev.json
├── public/ # Frontend assets and UI
│ ├── index.html
│ ├── splash.html
── assets/
│ │ ├── icon/ # Application icons
└── logo/ # Logo assets
│ ├── script/ # Frontend JavaScript modules
├── appVersion.js
├── clipboardPaste.js
│ │ ├── custompath.js
│ │ ├── customthemes.js
│ │ ├── downloadstatus.js
│ │ ├── fetchinfo.js
│ │ ├── progressBar.js
│ │ ├── settingsPanel.js
│ │ ├── toast.js
│ │ └── topbar.js
│ └── styles/ # CSS stylesheets
│ ├── styles.css
│ ├── variables.css
│ ├── components/ # Component-specific styles
│ │ ├── checkbox.css
│ │ ├── editpathbutton.css
│ │ ├── loader.css
│ │ ├── progressBar.css
│ │ └── toast.css
│ └── layout/ # Layout styles
│ ├── container.css
│ ├── form.css
│ ├── header.css
│ ├── settingsPanel.css
│ ├── topbar.css
│ └── videoinfo.css
├── ressources/ # Internal resources (binaries)
├── server/ # Express backend server
│ ├── logger.js
│ ├── server.js
│ ├── controller/ # Request handlers
│ │ ├── download.controller.js
│ │ └── info.controller.js
│ ├── helpers/ # Utility functions
│ │ ├── buildArgs.helpers.js
│ │ ├── getBrowser.helpers.js
│ │ ├── notify.helpers.js
│ │ ├── parseInfo.helpers.js
│ │ ├── path.helpers.js
│ │ ├── rateLimit.helpers.js
│ │ └── validation.helpers.js
│ ├── routes/ # API route definitions
│ │ ├── download.route.js
│ │ └── info.route.js
│ └── services/ # Business logic layer
│ ├── download.services.js
│ └── info.services.js
├── theme/ # Theme system
│ ├── template.theme.json # Theme template
│ ├── Dark/ # Default dark theme
│ │ └── dark.theme.json
│ └── Light/ # Default light theme
│ └── light.theme.json
├── main.js # Electron main process entry point
├── preload.js # Electron preload script (IPC bridge)
├── config.js # Global configuration loader
├── package.json # Project metadata and dependencies
├── CODE_OF_CONDUCT.md # Community guidelines
├── CONTRIBUTING.md # Contribution guidelines
├── LICENSE # GPLv3 license
└── README.md # This file
```
### Architecture Overview
@@ -230,10 +312,25 @@ Freedom Loader uses a client-server architecture within a single Electron applic
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
- **Backend**: Express.js server running locally for download management
- **App modules**: Electron-specific logic (window, IPC, updates, RPC) isolated in `app/`
- **IPC Bridge**: Secure communication via Electron's preload script
- **Logging**: Winston-based structured logging with file rotation
- **Updates**: Automatic checking and installation via electron-updater
## Theme Workshop
Freedom Loader includes a web-based theme creator available at [Freedom Loader Workshop](https://masteracnolo.github.io/Freedom-Loader-Workshop/).
- Create custom themes with a live preview of the actual UI
- Browse and download community themes
- Export themes as `.zip` files ready to drop into the `theme/` folder
- Supports background images, custom colors for every UI element
### Installing a custom theme
1. Download a `.zip` theme file
2. Drop it in the `theme/` folder of your Freedom Loader installation
3. Restart the app — your theme appears in the settings panel
## Technology Stack
### Core Technologies
@@ -324,6 +421,8 @@ ressources/
## Roadmap
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
### Completed
- [x] ~~Website and documentation~~
@@ -334,11 +433,15 @@ ressources/
- [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.)
- [ ] Refactoring and cleaning the code base.
- [ ] Chrome/Edge cookie support
- [ ] Improved multi-site support
@@ -357,6 +460,11 @@ ressources/
Contributions are welcome and appreciated. This project benefits from community involvement.
## Submitting a community theme
Want your theme featured in the Workshop? Please **do not open an issue on this repository** for theme submissions.
Head over to the [Freedom Loader Workshop repository](https://github.com/MasterAcnolo/Freedom-Loader-Workshop) and open an issue there with your theme files, preview screenshots, and description.
### Bug Reports
Use the GitHub Issues system and include:
@@ -408,6 +516,7 @@ You are free to use, modify, and redistribute this software under the terms of t
- [Electron](https://www.electronjs.org/) for the desktop application framework
- The open-source community for continuous support and contributions
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
- Zakaria for the website icon
- Jacques Chirac to love Apples
- All users who test, report issues, and help improve the application
@@ -417,6 +526,6 @@ You are free to use, modify, and redistribute this software under the terms of t
**Freedom Loader** - Put freedom in your downloads
[Website](https://masteracnolo.github.io/FreedomLoader/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
[Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
</div>

83
app/autoUpdater.js Normal file
View File

@@ -0,0 +1,83 @@
const { autoUpdater } = require("electron-updater");
const { dialog } = require("electron");
const { logger } = require("../server/logger");
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
function initAutoUpdater(mainWindow) {
autoUpdater.on("update-available", async (info) => {
logger.info(`Update available: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, {
type: "info",
title: "Update Available",
message: `Version ${info.version} is available.`,
detail: "Would you like to download and install it now?",
buttons: ["Install Update", "Maybe Later"],
defaultId: 0,
cancelId: 1,
});
if (response === 0) {
autoUpdater.downloadUpdate();
} else {
mainWindow?.webContents.executeJavaScript(
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
);
}
});
autoUpdater.on("download-progress", (progress) => {
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
mainWindow?.webContents.send("update-progress", {
percent: Math.round(progress.percent),
speed: progress.bytesPerSecond,
});
});
autoUpdater.on("update-downloaded", async (info) => {
logger.info(`Update downloaded: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, {
type: "info",
title: "Update Ready",
message: `Version ${info.version} has been downloaded.`,
detail: "The application will restart to apply the update.",
buttons: ["Install Now", "Later"],
defaultId: 0,
cancelId: 1,
});
if (response === 0) autoUpdater.quitAndInstall();
});
autoUpdater.on("error", (err) => {
logger.error("Auto update error:", err.message);
dialog.showErrorBox("Update Error", err.message);
});
checkForUpdates();
}
async function checkForUpdates() {
try {
await autoUpdater.checkForUpdates();
} catch (err) {
logger.error("Update check failed:", err.message);
}
}
async function downloadUpdate() {
try {
await autoUpdater.downloadUpdate();
} catch (err) {
logger.error("Download failed:", err.message);
}
}
function installUpdate() {
autoUpdater.quitAndInstall();
}
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };

32
app/dependencyCheck.js Normal file
View File

@@ -0,0 +1,32 @@
const fs = require("fs");
const { app, dialog } = require("electron");
const { logger } = require("../server/logger");
function checkNativeDependencies() {
const { binaryPaths } = require("../server/helpers/path.helpers");
const deps = [
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
{ name: "deno.exe", path: binaryPaths.deno },
];
const missing = deps.filter(d => !fs.existsSync(d.path));
if (missing.length === 0) return true;
const list = missing.map(d => d.name).join(", ");
logger.error(`Missing dependencies: ${list}`);
app.whenReady().then(() => {
dialog.showErrorBox(
"Missing dependencies",
`The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
);
app.quit();
});
return false;
}
module.exports = { checkNativeDependencies };

View File

@@ -1,6 +1,6 @@
const config = require('../config');
const RPC = require("discord-rpc");
const { logger } = require("./logger");
const { logger } = require("../server/logger");
const clientId = `${config.DiscordRPCID}`;
const rpc = new RPC.Client({ transport: "ipc" });

127
app/ipcHandlers.js Normal file
View File

@@ -0,0 +1,127 @@
const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs");
const path = require("path");
const { logger, logDir } = require("../server/logger");
const { configFeatures, featuresPath } = require("../config");
const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
const { userThemesPath } = require("../server/helpers/path.helpers");
const FEATURE_WHITELIST = new Set([
"autoUpdate",
"discordRPC",
"customTopBar",
"autoCheckInfo",
"addThumbnail",
"addMetadata",
"verboseLogs",
"autoDownloadPlaylist",
"customCodec",
"theme",
"createPlaylistFolders",
"notifySystem"
]);
const configFolderPath = featuresPath;
const themeFolderPath = userThemesPath;
function registerIpcHandlers(getMainWindow) {
// Infos générales
ipcMain.handle("version", () => config.version);
ipcMain.handle("features", () => configFeatures);
// Sélection et validation de dossier
ipcMain.handle("select-download-folder", async () => {
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (result.canceled) {
logger.info("Folder selection cancelled by user");
return null;
}
try {
const validated = validateDownloadPath(result.filePaths[0]);
logger.info(`Folder selected and validated: ${validated}`);
return validated;
} catch (err) {
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
throw err;
}
});
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
// Progression dans la taskbar
ipcMain.on("set-progress", (_, percent) => {
getMainWindow()?.setProgressBar(percent / 100);
});
// TOPBAR ACTION
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
ipcMain.on("window-maximize", () => {
const win = getMainWindow();
if (!win) return;
win.isMaximized() ? win.unmaximize() : win.maximize();
});
ipcMain.on("window-close", () => getMainWindow()?.close());
ipcMain.on("open-devtools", () =>
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
);
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
ipcMain.on("open-website", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
);
ipcMain.on("open-wiki", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
);
ipcMain.on("open-workshop", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
);
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
// THEME
ipcMain.handle("get-themes", () => getThemes());
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
ipcMain.handle("reload-themes", async () => {
return await reloadThemes();
});
// Modification des features
ipcMain.handle("set-feature", (event, { key, value }) => {
try {
if (!FEATURE_WHITELIST.has(key)) {
logger.warn(`Rejected feature (not whitelisted): ${key}`);
return false;
}
if (configFeatures[key] === value) {
return true;
}
configFeatures[key] = value;
fs.writeFileSync(
configFolderPath,
JSON.stringify(configFeatures, null, 2),
"utf-8"
);
logger.info(`Feature updated: ${key} = ${value}`);
return true;
} catch (err) {
logger.error(`set-feature failed (${key}): ${err.message}`);
return false;
}
});
}
module.exports = { registerIpcHandlers };

32
app/pathValidator.js Normal file
View File

@@ -0,0 +1,32 @@
const fs = require("fs");
const path = require("path");
const { logger } = require("../server/logger");
let _defaultPath = null;
function getDefaultDownloadPath() {
if (!_defaultPath) {
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
_defaultPath = defaultDownloadFolder;
}
return _defaultPath;
}
function validateDownloadPath(userPath) {
const { isSafePath } = require("../server/helpers/validation.helpers");
if (!userPath) return getDefaultDownloadPath();
try {
const resolved = fs.realpathSync(path.resolve(userPath));
if (!isSafePath(resolved)) {
throw new Error("Path not allowed: system folders are blocked!");
}
return resolved;
} catch (err) {
logger.error(`Invalid download path: ${userPath}${err.message}`);
throw new Error(`Invalid or inaccessible path: ${err.message}`);
}
}
module.exports = { validateDownloadPath, getDefaultDownloadPath };

58
app/splashManager.js Normal file
View File

@@ -0,0 +1,58 @@
const { BrowserWindow, app } = require("electron");
const path = require("path");
let splashWindow = null;
function createSplashWindow() {
const splashOptions = {
width: 400,
height: 300,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
};
splashWindow = new BrowserWindow(splashOptions);
const splashPath = path.join(__dirname, "../public/splash.html");
splashWindow.loadFile(splashPath);
// Inject banner path for both dev and packaged app
splashWindow.webContents.on('did-finish-load', () => {
let bannerPath;
// Check if app is packaged
if (app.isPackaged) {
bannerPath = path.join(process.resourcesPath, 'banner.png');
} else {
// In dev, use build folder
bannerPath = path.join(__dirname, '../build/banner.png');
}
splashWindow.webContents.executeJavaScript(`
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
`);
});
}
function closeSplashWindow() {
if (splashWindow) {
splashWindow.close();
splashWindow = null;
}
}
function setSplashProgress(step) {
if (splashWindow) {
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
}
}
module.exports = { createSplashWindow, closeSplashWindow, setSplashProgress };

188
app/themeManager.js Normal file
View File

@@ -0,0 +1,188 @@
const fs = require("fs");
const path = require("path");
const { logger } = require("../server/logger");
const JSZip = require("jszip");
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
const ALLOWED_IMAGE_NAMES = ["cover", "background"];
const THEME_ORDER = ["dark", "light"];
const REQUIRED_KEYS = [
["meta", "name"],
["meta", "author"],
["meta", "version"],
["meta", "formatVersion"],
["style", "colors", "background"],
["style", "colors", "text", "default"],
["style", "form", "button", "background"],
["style", "progressBar", "fill"],
];
function getNestedValue(obj, keys) {
return keys.reduce((acc, key) => acc?.[key], obj);
}
function validateThemeJson(json) {
for (const keyPath of REQUIRED_KEYS) {
if (getNestedValue(json, keyPath) === undefined) {
return { valid: false, reason: `Missing key: ${keyPath.join(".")}` };
}
}
return { valid: true };
}
function buildThemeObject(themeId, themeJson, imageData) {
return {
id: themeId.toLowerCase(),
name: themeJson.meta.name || themeId,
author: themeJson.meta.author || "Unknown",
version: themeJson.meta.version,
subtitle: themeJson.meta.subtitle || "",
style: themeJson.style,
image: imageData,
};
}
function extractImage(buffer, filename) {
if (buffer.length > MAX_IMAGE_SIZE) {
logger.warn(`Image too large, ignoring`);
return null;
}
const ext = path.extname(filename).toLowerCase().replace(".", "");
const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
return `data:image/${mime};base64,${buffer.toString("base64")}`;
}
async function loadThemeFromZip(zipPath, themeId) {
try {
const zipBuffer = fs.readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBuffer);
const jsonFile = Object.keys(zip.files).find(f => f.endsWith(".theme.json"));
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
let themeJson;
try { themeJson = JSON.parse(await zip.files[jsonFile].async("string")); }
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
const validation = validateThemeJson(themeJson);
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
let imageData = null;
const imageFile = Object.keys(zip.files).find(f => {
const ext = path.extname(f).toLowerCase();
const name = path.basename(f, ext).toLowerCase();
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
});
if (imageFile) {
const buf = await zip.files[imageFile].async("nodebuffer");
imageData = extractImage(buf, imageFile);
}
logger.info(`Theme loaded (zip): ${themeId}`);
return buildThemeObject(themeId, themeJson, imageData);
} catch (err) {
logger.warn(`Theme ${themeId}: failed to load zip — ${err.message}`);
return null;
}
}
async function loadThemeFromFolder(folderPath, themeId) {
try {
const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
let themeJson;
try { themeJson = JSON.parse(fs.readFileSync(path.join(folderPath, jsonFile), "utf-8")); }
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
const validation = validateThemeJson(themeJson);
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
let imageData = null;
const imageFile = fs.readdirSync(folderPath).find(f => {
const ext = path.extname(f).toLowerCase();
const name = path.basename(f, ext).toLowerCase();
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
});
if (imageFile) {
const buf = fs.readFileSync(path.join(folderPath, imageFile));
imageData = extractImage(buf, imageFile);
}
logger.info(`Theme loaded (folder): ${themeId}`);
return buildThemeObject(themeId, themeJson, imageData);
} catch (err) {
logger.warn(`Theme ${themeId}: failed to load folder — ${err.message}`);
return null;
}
}
let cachedThemes = null;
let themeFolderPath = null;
function setThemeFolderPath(folderPath) {
themeFolderPath = folderPath;
}
async function loadThemesFromFolder() {
const themes = [];
if (!fs.existsSync(themeFolderPath)) {
logger.warn(`Theme folder not found: ${themeFolderPath}`);
return themes;
}
const files = fs.readdirSync(themeFolderPath);
for (const file of files) {
const filePath = path.join(themeFolderPath, file);
const themeId = path.basename(file, path.extname(file));
if (file.endsWith(".zip")) {
const theme = await loadThemeFromZip(filePath, themeId);
if (theme) themes.push(theme);
continue;
}
if (fs.statSync(filePath).isDirectory()) {
const theme = await loadThemeFromFolder(filePath, themeId);
if (theme) themes.push(theme);
continue;
}
}
return themes;
}
function getThemeOrder(id) {
const index = THEME_ORDER.indexOf(id);
return index === -1 ? Infinity : index;
}
async function initThemes(folderPath) {
setThemeFolderPath(folderPath);
const themes = await loadThemesFromFolder();
logger.info(`Themes before sort: ${themes.map(t => t.id).join(", ")}`);
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`);
}
function getThemes() {
return cachedThemes ?? [];
}
async function reloadThemes() {
const themes = await loadThemesFromFolder();
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
logger.info(`Themes reloaded: ${cachedThemes.length} theme(s)`);
return cachedThemes;
}
module.exports = { initThemes, getThemes , reloadThemes };

58
app/windowManager.js Normal file
View File

@@ -0,0 +1,58 @@
const { BrowserWindow, app } = require("electron");
const path = require("path");
const { logger } = require("../server/logger");
const { configFeatures } = require("../config");
const config = require("../config");
let mainWindow = null;
async function createMainWindow() {
if (mainWindow) {
logger.warn("Window already exists, no new creation!");
return mainWindow;
}
const iconPath = config.localMode
? path.join(__dirname, "../build/app-icon.ico")
: path.join(process.resourcesPath, "build/app-icon.ico");
const windowOptions = {
title: `Freedom Loader ${config.version}`,
icon: iconPath,
width: 750,
height: 800,
minWidth: 750,
minHeight: 800,
frame: !configFeatures.customTopBar,
devTools: !app.isPackaged,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "../preload.js"),
},
};
mainWindow = new BrowserWindow(windowOptions);
try {
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
logger.info("Window loaded");
} catch (err) {
logger.error("Window loading error:", err);
throw err;
}
mainWindow.on("closed", () => {
logger.info("Main window closed");
mainWindow = null;
});
return mainWindow;
}
function getMainWindow() {
return mainWindow;
}
module.exports = { createMainWindow, getMainWindow };

12
app/ytDlpUpdater.js Normal file
View File

@@ -0,0 +1,12 @@
const { execFile } = require("child_process");
const { logger } = require("../server/logger");
function updateYtDlp(ytDlpPath) {
logger.info("yt-dlp update check starting...");
execFile(ytDlpPath, ["-U"], (err, stdout) => {
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
else logger.info(`yt-dlp update: ${stdout.trim()}`);
});
}
module.exports = { updateYtDlp };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -5,15 +5,30 @@ const path = require("path");
const localMode = !app.isPackaged;
// Change path for JSON file
const featuresPath = path.join(path.join(`${localMode ? __dirname : process.resourcesPath}`,"config/" , "config.json"));
function resolveConfigPath() {
if (localMode) {
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
if (!fs.existsSync(devConfigPath)) {
const defaultConfigPath = path.join(__dirname, "config", "config.default.json");
fs.copyFileSync(defaultConfigPath, devConfigPath);
}
return devConfigPath;
}
let features = {};
const userConfigPath = path.join(app.getPath("userData"), "config.json");
if (!fs.existsSync(userConfigPath)) {
const defaultConfigPath = path.join(process.resourcesPath, "config", "config.default.json");
fs.copyFileSync(defaultConfigPath, userConfigPath);
}
return userConfigPath;
}
const featuresPath = resolveConfigPath();
function loadFeatures() {
const raw = fs.readFileSync(featuresPath, "utf-8");
features = JSON.parse(raw);
return features;
return JSON.parse(raw);
}
const configFeatures = loadFeatures();
@@ -23,5 +38,6 @@ module.exports = {
applicationPort: "8787",
localMode,
DiscordRPCID: "1410934537051181146",
configFeatures
configFeatures,
featuresPath
}

View File

@@ -7,9 +7,11 @@
"addMetadata": true,
"verboseLogs": false,
"autoDownloadPlaylist": true,
"createPlaylistFolders": true,
"customCodec": "h264",
"logSystem": true,
"outputTitleCheck": true,
"downloadSystem": true,
"notifySystem": true
"notifySystem": true,
"theme": "dark"
}

317
main.js
View File

@@ -1,301 +1,90 @@
const config = require("./config.js");
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
app.quit();
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
app.quit();
});
const { app } = require("electron");
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
const path = require("path");
const fs = require("fs");
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
const { AutoUpdater } = require("./server/update.js");
const { configFeatures } = require("./config.js");
const { startRPC, stopRPC} = require("./server/discordRPC");
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
const { initAutoUpdater } = require("./app/autoUpdater");
const { startRPC, stopRPC } = require("./app/discordRPC");
let mainWindow;
const logsFolderPath = logDir;
const { configFeatures } = require("./config");
const config = require("./config");
const { initThemes } = require("./app/themeManager");
const basePath = config.localMode
? path.join(__dirname )
: path.join(path.dirname(process.execPath), "resources");
const { checkNativeDependencies } = require("./app/dependencyCheck");
const { updateYtDlp } = require("./app/ytDlpUpdater");
const { createMainWindow, getMainWindow } = require("./app/windowManager");
const { registerIpcHandlers } = require("./app/ipcHandlers");
const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
const configFolderPath = path.join(basePath, "config" ,"config.json");
// Default download path (centralized, lazy loaded)
let defaultDownloadPath;
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
app.setName("Freedom Loader");
app.setAppUserModelId("com.masteracnolo.freedomloader");
app.disableHardwareAcceleration();
ipcMain.handle("version", () => config.version);
// Gestion single instance
const gotLock = app.requestSingleInstanceLock();
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
function checkNativeDependencies() {
// Import centralized paths after app initialization
const { binaryPaths } = require("./server/helpers/path.helpers.js");
const deps = [
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
{ name: "deno.exe", path: binaryPaths.deno },
];
const missing = deps.filter(dep => !fs.existsSync(dep.path));
let errorMsg = "";
if (missing.length > 0) {
const missingList = missing.map(dep => dep.name).join(", ");
logger.error(`Missing dependencies: ${missingList}`);
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
}
if (errorMsg) {
app.whenReady().then(() => {
dialog.showErrorBox(
"Missing dependencies",
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
);
app.quit();
});
return false;
}
return true;
}
if(!config.localMode){
if (!gotLock) {
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
// Ici la nouvelle instance continue normalement
} else {
if (!checkNativeDependencies()) {
// Arrêt déjà géré dans la fonction
} else {
if (!config.localMode) {
const gotLock = app.requestSingleInstanceLock();
if (gotLock) {
app.on("second-instance", () => {
// La vieille instance se ferme
if (mainWindow) {
logger.info("New Instance Detected, closing the older...");
mainWindow.destroy();
mainWindow = null;
}
logger.info("New instance detected, closing older...");
getMainWindow()?.destroy();
});
}
}
}
// Création fenêtre principale
async function createMainWindow() {
if (mainWindow) {
logger.warn("Window already exists, no new creation!");
return;
}
mainWindow = new BrowserWindow({
title: `Freedom Loader ${config.version}`,
width: 750,
height: 800,
minWidth: 750,
minHeight: 800,
frame: !configFeatures.customTopBar,
devTools: `${app.isPackaged ? false : true}`,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
},
});
try {
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
logger.info("Window Loaded");
} catch (err) {
logger.error("Window Loading Error:", err);
}
mainWindow.on("closed", () => {
logger.info("Main Window Closed");
mainWindow = null;
});
}
function validateDownloadPath(userPath) {
const { isSafePath } = require("./server/helpers/validation.helpers.js");
// Lazy load default path
if (!defaultDownloadPath) {
const { defaultDownloadFolder } = require("./server/helpers/path.helpers.js");
defaultDownloadPath = defaultDownloadFolder;
}
if (!userPath) return defaultDownloadPath;
try {
// Canonical resolution and symlink following
const resolved = fs.realpathSync(path.resolve(userPath));
// Use the same validation as backend (allows all drives except system folders)
if (!isSafePath(resolved)) {
throw new Error("Path not allowed: system folders are blocked!");
}
return resolved;
} catch (err) {
logger.error(`Invalid download path: ${userPath} - ${err.message}`);
throw new Error(`Invalid or inaccessible path: ${err.message}`);
}
}
// IPC
ipcMain.handle("select-download-folder", async () => {
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (result.canceled) {
logger.info("Folder selection cancelled by user");
return null;
}
if (result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
try {
const validatedPath = validateDownloadPath(selectedPath);
logger.info(`Folder selected and validated: ${validatedPath}`);
return validatedPath;
} catch (err) {
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
throw err; // Propagate error to UI
}
}
return null;
});
ipcMain.handle("validate-download-path", (event, userPath) => {
return validateDownloadPath(userPath);
});
ipcMain.handle("get-default-download-path", () => {
if (!defaultDownloadPath) {
const { defaultDownloadFolder } = require("./server/helpers/path");
defaultDownloadPath = defaultDownloadFolder;
}
return defaultDownloadPath;
});
ipcMain.on("set-progress", (event, percent) => {
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
});
// Topbar window controls
ipcMain.on("window-minimize", () => {
if (mainWindow) mainWindow.minimize();
});
// Toggle Maximize -> UnMaximize
ipcMain.on("window-maximize", () => {
if (mainWindow) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.on("window-close", () => {
if (mainWindow) mainWindow.close();
});
// Topbar custom actions
ipcMain.on("open-devtools", () => {
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
});
ipcMain.on("open-logs", () => {
if (logsFolderPath) shell.openPath(logsFolderPath);
});
ipcMain.on("open-website", () => {
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
});
ipcMain.on("open-wiki", () => {
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
});
ipcMain.on("open-config", () => {
if (configFolderPath) shell.openPath(configFolderPath);
});
// App ready
app.whenReady().then(async () => {
logSessionStart();
logger.info("App Ready, Server Express starting...");
const serverPath = path.join(__dirname, "server", "server.js")
createSplashWindow();
const expressServer = require(serverPath);
if (!config.localMode && !checkNativeDependencies()) return;
const { userYtDlp } = require("./server/helpers/path.helpers");
updateYtDlp(userYtDlp);
try {
await expressServer.startServer();
logger.info("Express Server Started");
setSplashProgress(0); // Checking dependencies
await require("./server/server").startServer();
ipcMain.handle("features", () => {
return configFeatures;
});
setSplashProgress(1); // Starting server
registerIpcHandlers(getMainWindow);
const featureWhitelist = new Set([
"autoUpdate",
"discordRPC",
"customTopBar",
"autoCheckInfo",
"addThumbnail",
"addMetadata",
"verboseLogs",
"autoDownloadPlaylist",
"customCodec"
]);
ipcMain.handle("set-feature", (event, { key, value }) => {
try {
if (!featureWhitelist.has(key)) {
logger.warn(`Rejected feature (not whitelisted): ${key}`);
return false;
}
// optionnel mais propre
if (configFeatures[key] === value) {
return true;
}
configFeatures[key] = value;
fs.writeFileSync(
configFolderPath,
JSON.stringify(configFeatures, null, 2),
"utf-8"
);
logger.info(`Feature updated: ${key} = ${value}`);
return true;
} catch (err) {
logger.error(`set-feature failed (${key}): ${err.message}`);
return false;
}
});
if (configFeatures.discordRPC) startRPC(); // Discord RPC
setSplashProgress(2); // Loading themes
initUserThemes();
await initThemes(userThemesPath);
setSplashProgress(3); // Almost ready
await createMainWindow();
if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update
await new Promise(resolve => setTimeout(resolve, 2000));
closeSplashWindow();
getMainWindow().show();
if (configFeatures.discordRPC) startRPC();
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
} catch (err) {
logger.error("Window or Server error :", err);
logger.error("Boot error:", err);
app.quit();
}
});
app.on("window-all-closed", () => {
logger.info("Shuting Down App...");
logger.info("Shutting down...");
app.quit();
});
app.on("before-quit", async () => {
await stopRPC();
logger.info("All Services Stopped. Have a nice day!")
logger.info("All services stopped. Have a nice day!");
logSessionEnd();
});

240
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "freedom-loader",
"version": "1.4.5",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freedom-loader",
"version": "1.4.5",
"version": "1.5.0",
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
@@ -14,12 +14,13 @@
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"jszip": "^3.10.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"electron": "^37.1.0",
"electron": "^41.2.0",
"electron-builder": "^26.8.1"
}
},
@@ -87,9 +88,9 @@
"license": "MIT"
},
"node_modules/@electron/asar/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -381,9 +382,9 @@
"license": "MIT"
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -815,9 +816,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -859,13 +860,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.16.0"
}
},
"node_modules/@types/plist": {
@@ -916,9 +917,9 @@
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
"version": "0.8.12",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1354,9 +1355,9 @@
"optional": true
},
"node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1525,9 +1526,9 @@
"license": "MIT"
},
"node_modules/cacache/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1882,9 +1883,9 @@
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1925,9 +1926,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/crc": {
"version": "3.8.0",
@@ -2150,9 +2149,9 @@
"license": "MIT"
},
"node_modules/dir-compare/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2341,15 +2340,15 @@
}
},
"node_modules/electron": {
"version": "37.10.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
"version": "41.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz",
"integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"@types/node": "^24.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -2795,9 +2794,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
@@ -2926,9 +2925,9 @@
"license": "MIT"
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3200,9 +3199,9 @@
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3515,6 +3514,12 @@
],
"license": "BSD-3-Clause"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -3612,6 +3617,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isbinaryfile": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
@@ -3736,6 +3747,48 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3758,10 +3811,19 @@
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"license": "MIT"
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
@@ -3961,13 +4023,13 @@
}
},
"node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -4028,11 +4090,11 @@
}
},
"node_modules/minipass-flush": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz",
"integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"minipass": "^3.0.0"
},
@@ -4447,6 +4509,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -4501,9 +4569,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -4540,9 +4608,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4607,6 +4675,12 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -4941,9 +5015,9 @@
"license": "MIT"
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz",
"integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==",
"dev": true,
"license": "WTFPL OR ISC",
"dependencies": {
@@ -4951,9 +5025,9 @@
}
},
"node_modules/sax": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
"integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
@@ -5039,6 +5113,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -5393,9 +5473,9 @@
}
},
"node_modules/tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -5516,14 +5596,14 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -5615,9 +5695,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,7 +1,7 @@
{
"name": "freedom-loader",
"productName": "Freedom Loader",
"version": "1.4.6",
"version": "1.5.0",
"author": "MasterAcnolo",
"description": "Freedom Loader",
"main": "main.js",
@@ -17,12 +17,13 @@
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"jszip": "^3.10.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"electron": "^37.1.0",
"electron": "^41.2.0",
"electron-builder": "^26.8.1"
},
"build": {
@@ -38,13 +39,18 @@
"files": [
"**/*",
"node_modules/**/*",
"!logs/*.json"
"!config/config.dev.json",
"!theme/**/*"
],
"directories": {
"buildResources": "build"
},
"asar": true,
"extraResources": [
{
"from": "theme",
"to": "theme"
},
{
"from": "build/confirm-icon.png",
"to": "confirm-icon.png"
@@ -57,6 +63,10 @@
"from": "build/banner.bmp",
"to": "banner.bmp"
},
{
"from": "build/banner.png",
"to": "banner.png"
},
{
"from": "build/error.png",
"to": "error.png"
@@ -78,8 +88,8 @@
"to": "binaries/deno.exe"
},
{
"from": "config/config.json",
"to": "config/config.json"
"from": "config/config.default.json",
"to": "config/config.default.json"
}
],
"win": {

View File

@@ -7,7 +7,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
getFeatures: () => ipcRenderer.invoke("features"),
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
getVersion: () => ipcRenderer.invoke("version"),
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path),
getThemes: () => ipcRenderer.invoke("get-themes"),
reloadThemes: () => ipcRenderer.invoke("reload-themes")
});
// Contrôles de fenêtre et outils custom pour la topbar
@@ -18,6 +20,8 @@ contextBridge.exposeInMainWorld("topbarAPI", {
openDevTools: () => ipcRenderer.send("open-devtools"),
openLogs: () => ipcRenderer.send("open-logs"),
openWebsite: () => ipcRenderer.send("open-website"),
openTheme: () => ipcRenderer.send("open-theme"),
openWiki: () => ipcRenderer.send("open-wiki"),
openWorkshop: () => ipcRenderer.send("open-workshop"),
openConfig: () => ipcRenderer.send("open-config")
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

View File

@@ -9,7 +9,6 @@
font-src https://fonts.gstatic.com;
img-src 'self' data: https:;
">
<title></title>
<link rel="stylesheet" href="styles/styles.css">
<link rel="stylesheet" href="styles/layout/topbar.css">
@@ -17,12 +16,15 @@
</head>
<body>
<header>
<div class="topbar" id="topbar">
<div class="custom-controls">
<button id="devtools-btn" title="Dev Tools">Tools</button>
<button id="logs-btn" title="Logs">Logs</button>
<button id="website-btn" title="Website">Website</button>
<button id="wiki-btn" title="Wiki">Wiki</button>
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
</div>
<div class="window-controls">
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
@@ -30,6 +32,7 @@
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
</div>
</div>
</header>
<button class="settings-button" id="settings-button">
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
@@ -52,12 +55,18 @@
<div class="setting-item" id="theme" style="margin-left: 32px;">
<div class="setting-info">
<div style="display: flex;">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="setting-title">Thème</span>
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
</select>
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<small>Choose the theme of the application</small>
</div>
@@ -80,20 +89,20 @@
</div>
</div>
<!-- <div class="setting-item">
<div class="setting-item">
<input type="checkbox" data-key="createPlaylistFolders" id="createPlaylistFolders">
<div class="setting-info">
<span class="setting-title">Create Playlist Folders</span>
<small>Create a dedicated folder for each playlist with its name.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="notifySystem" id="notifySystem">
<div class="setting-info">
<span class="setting-title">Notification at the end</span>
<small>Notify when a download finishes.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info">
<span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small>
</div>
</div>
<div class="setting-item">
@@ -142,6 +151,14 @@
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info">
<span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small>
</div>
</div>
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
<div class="setting-info">
<span class="setting-title">Custom Codec</span>
@@ -191,6 +208,7 @@
</details>
<div class="settings-footer">
<button id="open-theme">Open Theme Folder</button>
<button id="open-json-btn">Open JSON Configuration</button>
</div>
@@ -198,12 +216,10 @@
</div>
</div>
<main>
<div class="container" id="container">
<header>
<h1 id="title"> Freedom Loader </h1>
<p id="subtitle"> Because why not? </p>
</header>
<form action="/download" method="POST" id="downloadForm">
<div class="url-input-container">
@@ -234,6 +250,7 @@
<button type="submit" onclick="startProgress()">Download</button>
</form>
</div>
<div class="download-path">
@@ -241,16 +258,20 @@
<button id="changePath">Edit</button>
</div>
<div class="progress-container">
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
<div id="downloadProgressWrapper">
<div id="downloadProgress"></div>
</div>
<div style="display: flex; gap: 20px;">
<div style="display: flex; gap: 20px; align-items: center;">
<div id="downloadProgressText"></div>
<div id="downloadSpeedText"></div>
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download"></button>
</div>
</div>
<div id="downloadStatus"></div>
<div id="downloadStatus" style="display: none;"></div>
<div class="infos-container">
@@ -266,10 +287,17 @@
<span id="version-badge" class="version-badge"></span>
</div>
</main>
<!-- Toast Container -->
<div id="toast-container"></div>
<h2 id="reference"> Because Why Not ? </h2>
<!-- Scripts -->
<script src="script/toast.js"></script>
<script src="script/clipboardPaste.js"></script>
<script src="script/custompath.js"></script>
<script src="script/settingsPanel.js"></script>

View File

@@ -14,6 +14,7 @@ window.addEventListener("DOMContentLoaded", async () => {
async function applyPathFromBack(path) {
savePathElem.textContent = path;
savePathElem.title = path;
hidden.value = path;
localStorage.setItem("customDownloadPath", path); // UX only
}

View File

@@ -1,62 +1,129 @@
const themes = {
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
neon: { label: "Purple", subtitle: "How was your day ?"},
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
};
const themeSelect = document.getElementById("themeSelect");
let loadedThemes = [];
function populateThemeSelect() {
for (const [themeKey, themeInfo] of Object.entries(themes)) {
function applyTheme(theme) {
const root = document.documentElement;
const style = theme.style;
// Background
if (theme.image) {
document.body.style.backgroundImage = `url('${theme.image}')`;
document.body.style.backgroundSize = style.background?.size || "cover";
document.body.style.backgroundPosition = style.background?.position || "center";
document.body.style.backgroundAttachment = style.background?.attachment || "fixed";
} else {
document.body.style.backgroundImage = "";
}
// Colors
root.style.setProperty("--background-color", style.colors?.background || "");
root.style.setProperty("--default-text-color", style.colors?.text?.default || "");
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "");
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "");
// Form
root.style.setProperty("--form-bg-color", style.form?.background || "");
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "");
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "");
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "");
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "");
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "");
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "");
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "");
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "");
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "");
// Checkbox
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "");
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "");
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "");
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "");
// Download
root.style.setProperty("--download-status-color", style.download?.status || "");
// Progress bar
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "");
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "");
// Video info
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "");
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "");
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "");
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "");
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "");
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "");
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "");
// Playlist
root.style.setProperty("--playlist-background-color", style.playlist?.background || "");
// Settings
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "");
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "");
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "");
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "");
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "");
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "");
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "");
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "");
root.style.setProperty("--settings-text-color", style.settings?.text || "");
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "");
// Subtitle
const subtitleEl = document.getElementById("subtitle");
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
}
function saveTheme(themeId) {
window.electronAPI.setFeature("theme", themeId);
}
function populateThemeSelect(themes) {
themeSelect.innerHTML = "";
for (const theme of themes) {
const option = document.createElement("option");
option.value = themeKey;
option.textContent = themeInfo.label;
option.value = theme.id;
option.textContent = theme.name;
themeSelect.appendChild(option);
}
}
function applyTheme(themeKey) {
document.body.classList.remove(...Object.keys(themes));
document.body.classList.add(themeKey);
async function initThemes() {
loadedThemes = await window.electronAPI.getThemes();
const subtitleElement = document.getElementById("subtitle");
if (subtitleElement && themes[themeKey]) {
subtitleElement.textContent = themes[themeKey].subtitle;
}
if (!loadedThemes.length) return;
populateThemeSelect(loadedThemes);
const features = await window.electronAPI.getFeatures();
const savedId = features.theme;
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
themeSelect.value = theme.id;
applyTheme(theme);
}
function saveTheme(themeKey) {
localStorage.setItem("selectedTheme", themeKey);
}
function loadTheme() {
const savedTheme = localStorage.getItem("selectedTheme");
if (savedTheme && themes[savedTheme]) {
applyTheme(savedTheme);
themeSelect.value = savedTheme;
} else {
applyTheme("dark");
themeSelect.value = "dark";
}
}
// When the user changes the theme from the select
themeSelect.addEventListener("change", (event) => {
const selectedTheme = event.target.value;
if (themes[selectedTheme]) {
applyTheme(selectedTheme);
saveTheme(selectedTheme);
themeSelect.addEventListener("change", (e) => {
const theme = loadedThemes.find(t => t.id === e.target.value);
if (theme) {
applyTheme(theme);
saveTheme(theme.id);
}
});
// Initialisation
populateThemeSelect();
loadTheme();
async function refreshThemes() {
loadedThemes = await window.electronAPI.reloadThemes();
populateThemeSelect(loadedThemes);
const features = await window.electronAPI.getFeatures();
const savedId = features.theme;
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
themeSelect.value = theme.id;
applyTheme(theme);
}
window.refreshThemes = refreshThemes;
initThemes();

View File

@@ -1,11 +1,29 @@
const form = document.getElementById("downloadForm");
const statusDiv = document.getElementById("downloadStatus");
const button = form.querySelector("button");
const button = form.querySelector("button[type=\"submit\"]");
const stopBtn = document.getElementById("stopBtn");
let isDownloading = false;
let userStoppedDownload = false;
form.addEventListener("submit", async (e) => {
e.preventDefault();
button.disabled = true; // Avoid multiple clicks
statusDiv.textContent = "Download in progress...";
const urlInput = form.querySelector("input[name='url']");
const url = urlInput.value.trim();
// Basic URL validation
if (!url) {
window.showError("Please enter a URL");
return;
}
button.disabled = true;
statusDiv.style.display = "none";
isDownloading = true;
userStoppedDownload = false;
// Show progress UI immediately with initial message
showInitialProgress();
const formData = new FormData(form);
const params = new URLSearchParams(formData);
@@ -18,20 +36,92 @@ form.addEventListener("submit", async (e) => {
});
if (!res.ok) {
statusDiv.textContent = res;
if (!userStoppedDownload) {
const errorText = await res.text();
window.showError(errorText);
resetProgressBar();
}
return;
}
const text = await res.text();
statusDiv.textContent = text;
// Download successful - check if notifications are enabled
const features = await window.electronAPI.getFeatures();
if (features.notifySystem) {
window.showSuccess("Download completed!");
}
} catch {
statusDiv.textContent = "❌ An Error has Occured.";
} catch (err) {
if (!isDownloading && !userStoppedDownload) {
statusDiv.style.display = "none";
}
} finally {
isDownloading = false;
button.disabled = false;
setTimeout(() => {
statusDiv.textContent = "";
}, 5000);
stopBtn.style.display = "none";
}
});
function showInitialProgress() {
const progressWrapper = document.getElementById("downloadProgressWrapper");
const progressBarText = document.getElementById("downloadProgressText");
const speedElement = document.getElementById("downloadSpeedText");
const stageElement = document.getElementById("downloadStage");
const stopBtn = document.getElementById("stopBtn");
if (progressWrapper) progressWrapper.style.display = "block";
if (progressBarText) {
progressBarText.style.display = "block";
progressBarText.innerHTML = "0%";
}
if (speedElement) {
speedElement.style.display = "block";
speedElement.textContent = "0 Mib/s";
}
if (stageElement) {
stageElement.style.display = "block";
stageElement.textContent = "Retrieving data...";
}
if (stopBtn) stopBtn.style.display = "inline-flex";
}
async function stopDownload() {
if (!isDownloading) return;
userStoppedDownload = true;
button.disabled = true;
stopBtn.disabled = true;
try {
await fetch("/download/cancel", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
window.showWarning("Download stopped");
} catch (err) {
console.error("Error stopping download:", err);
} finally {
isDownloading = false;
button.disabled = false;
stopBtn.disabled = false;
stopBtn.style.display = "none";
resetProgressBar();
}
}
function resetProgressBar() {
const progressWrapper = document.getElementById("downloadProgressWrapper");
if (progressWrapper) progressWrapper.style.display = "none";
const progressBar = document.getElementById("downloadProgress");
if (progressBar) progressBar.style.width = "0%";
const progressBarText = document.getElementById("downloadProgressText");
if (progressBarText) progressBarText.style.display = "none";
const speedElement = document.getElementById("downloadSpeedText");
if (speedElement) speedElement.style.display = "none";
const playlistInfoElement = document.getElementById("playlistInfoText");
if (playlistInfoElement) playlistInfoElement.style.display = "none";
}

View File

@@ -33,6 +33,17 @@ async function init() {
const urlInput = document.getElementById("UrlInput");
const infoDiv = document.getElementById("videoInfo");
const loaderBox = document.getElementById("loaderBox");
const form = document.getElementById("downloadForm");
// Input caché pour stocker le titre de la playlist
let playlistTitleInput = document.getElementById("playlistTitle");
if (!playlistTitleInput) {
playlistTitleInput = document.createElement("input");
playlistTitleInput.type = "hidden";
playlistTitleInput.name = "playlistTitle";
playlistTitleInput.id = "playlistTitle";
form.appendChild(playlistTitleInput);
}
let lastFetchedUrl = "";
@@ -47,6 +58,7 @@ async function init() {
if (!url || url.length < 2) {
infoDiv.innerHTML = "";
infoDiv.classList.remove("visible", "playlist-mode");
playlistTitleInput.value = "";
lastFetchedUrl = "";
return;
}
@@ -70,12 +82,14 @@ async function init() {
`;
infoDiv.classList.add("visible");
infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
loaderBox.style.display = "none";
return;
}
// ---------- PLAYLIST ----------
if (data.type === "playlist") {
playlistTitleInput.value = data.title;
infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = `
<div class="playlist-header">
@@ -194,6 +208,7 @@ async function init() {
}
infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
// ---------- VIDEO NORMALE ----------
const durationStr = data.duration

View File

@@ -3,19 +3,34 @@ const progressBar = document.getElementById("downloadProgress");
const progressBarText = document.getElementById("downloadProgressText")
const speedElement = document.getElementById("downloadSpeedText");
const stageElement = document.getElementById("downloadStage");
const playlistInfoElement = document.getElementById("playlistInfoText");
const speedEvt = new EventSource("/download/speed");
const stageEvt = new EventSource("/download/stage");
const playlistInfoEvt = new EventSource("/download/playlist-info");
function startProgress() {
progressWrapper.style.display = "block";
progressBar.style.width = "0%";
progressBarText.style.display = "block";
progressBarText.innerHTML = "0%";
speedElement.style.display = "block";
speedElement.innerHTML = "0 Mib/s";
}
function showProgress() {
progressWrapper.style.display = "block";
progressBarText.style.display = "block";
speedElement.style.display = "block";
// Show stop button
const stopBtn = document.getElementById("stopBtn");
if (stopBtn) stopBtn.style.display = "inline-flex";
}
function updateProgress(percent) {
// Show progress div and stop button only when percent > 0
if (percent > 0 && progressWrapper.style.display !== "block") {
showProgress();
}
progressBar.style.width = `${percent}%`;
progressBarText.innerHTML = `${percent}%`;
}
@@ -28,6 +43,16 @@ function resetProgress() {
speedElement.textContent = "0 Mib/s";
speedElement.style.display = "none";
stageElement.textContent = "";
stageElement.style.display = "none";
playlistInfoElement.textContent = "";
playlistInfoElement.style.display = "none";
// Hide stop button
const stopBtn = document.getElementById("stopBtn");
if (stopBtn) stopBtn.style.display = "none";
}
// SSE Connexion
@@ -40,8 +65,11 @@ evtSource.onmessage = e => {
}
if (e.data === "done") {
// Keep progress bar visible for better UX, let toast handle the feedback
setTimeout(() => {
resetProgress();
window.electronAPI.setProgress(-1);
}, 2000); // Wait 2s so user sees 100% progress
return;
}
@@ -50,10 +78,7 @@ evtSource.onmessage = e => {
if (!isNaN(percent)) {
updateProgress(percent);
window.electronAPI.setProgress(percent); // Update Task Bar
if (percent >= 100) setTimeout(() => {
resetProgress();
window.electronAPI.setProgress(-1); // Remove the bar
}, 500);
// Don't hide at 100%, wait for "done" signal instead
}
};
@@ -61,3 +86,13 @@ speedEvt.onmessage = e => {
speedElement.style.display = "block";
speedElement.textContent = e.data; // ex: "5.2MiB/s"
};
stageEvt.onmessage = e => {
stageElement.style.display = "block";
stageElement.textContent = e.data; // ex: "📥 Downloading..."
};
playlistInfoEvt.onmessage = e => {
playlistInfoElement.style.display = "block";
playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15"
};

View File

@@ -27,6 +27,7 @@ async function loadSettings() {
}
el.addEventListener("change", () => {
if (key === "theme") return;
let value = el.type === "checkbox" ? el.checked : el.value;
window.electronAPI.setFeature(key, value);
});
@@ -35,7 +36,22 @@ async function loadSettings() {
// Open The JSON
document.getElementById("open-json-btn").addEventListener("click", () => {
window.topbarAPI.openConfig(); // ton IPC existant
window.topbarAPI.openConfig();
});
// Open Theme Folder
document.getElementById("open-theme").addEventListener("click", () => {
window.topbarAPI.openTheme();
});
// Refresh Themes
document.getElementById("refresh-themes-btn").addEventListener("click", function() {
this.classList.add("spinning");
window.refreshThemes();
setTimeout(() => {
this.classList.remove("spinning");
}, 600);
});

54
public/script/toast.js Normal file
View File

@@ -0,0 +1,54 @@
window.showToast = function(message, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container');
if (!container) return;
// Create toast element
const toast = document.createElement('div');
toast.classList.add('toast', type);
// Icons for different types
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: ''
};
const icon = icons[type] || icons.info;
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" title="Close">×</button>
`;
// Add close functionality
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => removeToast(toast));
// Add to container
container.appendChild(toast);
// Auto remove after duration
if (duration > 0) {
setTimeout(() => removeToast(toast), duration);
}
return toast;
};
function removeToast(toast) {
if (!toast.parentElement) return;
toast.classList.add('removing');
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}
window.showSuccess = (message, duration = 4000) => window.showToast(message, 'success', duration);
window.showError = (message, duration = 5000) => window.showToast(message, 'error', duration);
window.showWarning = (message, duration = 4000) => window.showToast(message, 'warning', duration);
window.showInfo = (message, duration = 4000) => window.showToast(message, 'info', duration);

View File

@@ -1,8 +1,8 @@
// Ajout des listeners pour la topbar
function setupTopbarListeners() {
document.addEventListener('DOMContentLoaded', () => {
const { topbarAPI } = window;
if (!topbarAPI) return;
const minBtn = document.getElementById('minimize-btn');
const maxBtn = document.getElementById('maximize-btn');
const closeBtn = document.getElementById('close-btn');
@@ -10,7 +10,7 @@ function setupTopbarListeners() {
const logsBtn = document.getElementById('logs-btn');
const websiteBtn = document.getElementById('website-btn');
const wikiBtn = document.getElementById('wiki-btn');
const configBtn = document.getElementById('config-btn');
const workshopBtn = document.getElementById('workshop-btn');
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
@@ -19,11 +19,10 @@ function setupTopbarListeners() {
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
});
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
}
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
const features = await window.electronAPI.getFeatures();

94
public/splash.html Normal file
View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: transparent;
overflow: hidden;
}
.splash-container {
width: 100%;
height: 100%;
background: #D9D9D9;
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
font-family: "Segoe UI", sans-serif;
}
.progress-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.progress-track {
width: 180px;
height: 3px;
background: #d0d0d0;
border-radius: 99px;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 0%;
background: #1a1a1a;
border-radius: 99px;
transition: width 0.3s ease;
}
.status {
font-size: 14px;
color: #6d6d6d;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="splash-container">
<img src="./banner.png" alt="Freedom Loader" style="width: 90%;">
<div class="progress-wrapper">
<div class="progress-track">
<div class="progress-bar" id="progress-bar"></div>
</div>
<span class="status" id="status">Starting...</span>
</div>
</div>
<script>
const steps = [
"Checking dependencies...",
"Starting server...",
"Loading themes...",
"Almost ready...",
];
let current = 0;
const bar = document.getElementById("progress-bar");
const status = document.getElementById("status");
function advance(label, percent) {
bar.style.width = percent + "%";
status.textContent = label;
}
window.setProgress = (step) => {
if (step >= steps.length) {
advance("", 100);
return;
}
const percent = Math.round(((step + 1) / steps.length) * 100);
advance(steps[step], percent);
};
window.setProgress(0);
</script>
</body>
</html>

View File

@@ -15,13 +15,13 @@
}
.checkmark {
--clr: var(--checkbox-bg-checked);
--clr: var(--checkbox-checked-bg-color);
position: relative;
top: 0;
left: 0;
height: 1.3em;
width: 1.3em;
background-color: var(--checkbox-bg-default);
background-color: var(--checkbox-unchecked-bg-color);
border-radius: .5rem;
transition: background-color 300ms, border-radius 300ms;
}

View File

@@ -17,9 +17,11 @@
border-radius: 6px;
font-family: monospace;
border: 1px dashed var(--form-input-border-color);
word-break: break-all;
display: inline-block;
max-width: 80%;
display: inline-flex;
max-width: clamp(200px, 60vw, 600px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -4,9 +4,9 @@
gap: 10px;
padding: 10px 20px;
background: var(--form-bg-color);
border: 2px solid var(--mp3-text-color);
border: 2px solid var(--audio-only-label-color);
border-radius: 8px;
color: var(--mp3-text-color);
color: var(--audio-only-label-color);
font-weight: bold;
animation: pulse 1.2s infinite;
}

View File

@@ -1,22 +1,34 @@
/* Container pour isoler la progress bar */
.progress-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px 0;
margin-top: 20px;
}
/* Wrapper de la barre de téléchargement */
#downloadProgressWrapper {
width: 100%;
max-width: 400px;
height: 12px;
background-color: #444;
border-radius: 6px;
background-color: var(--progress-bar-bg-color, #cccccc);
border-radius: 10px;
overflow: hidden;
margin: 10px auto;
margin: 0;
display: none;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
box-sizing: border-box;
}
/* Barre de progression */
#downloadProgress {
height: 100%;
width: 0%;
background-color: var(--video-info-link-color, #4caf50);
border-radius: 6px 0 0 6px;
background-color: var(--progress-bar-fill-color, #007bff);
border-radius: 10px;
transition: width 0.2s ease;
}
@@ -25,7 +37,7 @@
font-size: 0.8rem;
color: #fff;
content: "";
display: none;
}
/* Responsive pour mobile */
@@ -39,3 +51,39 @@
font-size: 0.7rem;
}
}
/* Stop Button */
.stop-btn {
width: 40px;
height: 40px;
min-width: 40px;
padding: 0;
border: none;
border-radius: 8px;
background-color: var(--form-button-bg-color);
color: var(--form-button-text-color);
font-size: 20px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.stop-btn:hover:not(:disabled) {
background-color: var(--form-button-bg-hover-color);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
transform: scale(1.05);
}
.stop-btn:active:not(:disabled) {
transform: scale(0.96);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.stop-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -0,0 +1,136 @@
/* Toast Container */
#toast-container {
position: fixed;
top: 140px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 9999;
pointer-events: none;
}
/* Toast */
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background-color: rgba(50, 50, 50, 0.9);
border-radius: 6px;
color: #ffffff;
font-size: 0.9rem;
max-width: 220px;
min-width: 100px;
animation: slideIn 0.25s ease-out;
pointer-events: auto;
backdrop-filter: blur(8px);
}
.toast.success {
background-color: rgba(40, 167, 69, 0.15);
}
.toast.error {
background-color: rgba(220, 53, 69, 0.15);
}
.toast.warning {
background-color: rgba(255, 193, 7, 0.15);
}
.toast.info {
background-color: rgba(23, 162, 184, 0.15);
}
/* Toast Icon */
.toast-icon {
flex-shrink: 0;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.toast.success .toast-icon {
color: #28a745;
}
.toast.error .toast-icon {
color: #dc3545;
}
.toast.warning .toast-icon {
color: #ffc107;
}
.toast.info .toast-icon {
color: #17a2b8;
}
/* Toast Message */
.toast-message {
flex: 1;
font-weight: 500;
}
/* Toast Close Button */
.toast-close {
flex-shrink: 0;
background: none;
border: none;
color: currentColor;
cursor: pointer;
font-size: 18px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.toast-close:hover {
opacity: 1;
}
/* Animations */
@keyframes slideIn {
from {
transform: translateX(350px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(350px);
opacity: 0;
}
}
.toast.removing {
animation: slideOut 0.25s ease-out;
}
/* Responsive */
@media (max-width: 480px) {
#toast-container {
bottom: 12px;
right: 12px;
left: 12px;
}
.toast {
max-width: none;
min-width: auto;
}
}

View File

@@ -4,7 +4,6 @@
left: 0;
right: 0;
padding: 1em;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -50,7 +50,7 @@ form#downloadForm {
align-items: center;
align-self: center;
gap: 0.5em;
color: var(--form-input-text-color);
color: var(--audio-only-label-color);
}
#UrlInput {
@@ -108,6 +108,7 @@ form#downloadForm {
}
.paste-btn svg {
color: var(--paste-button-icon-color);
width: 30px;
height: 30px;
}

View File

@@ -1,11 +1,13 @@
header {
position: fixed;
top: 0;
width: 100%;
height: 48px;
z-index: 1100;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2vw;
}
header h1{
margin-bottom: 0.2vw;
#title {
color: var(--default-text-color);
}

View File

@@ -7,6 +7,7 @@
top: 60px;
right: 20px;
opacity: 0.8;
z-index: 15;
background-color: var(--settings-button-bg-color);
border-radius: 14px;
padding: 8px;
@@ -40,7 +41,7 @@
width: 40vw;
max-width: 320px;
max-height: 80vh;
background-color: var(--settings-bg-primary-color);
background-color: var(--settings-modal-bg-color);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
@@ -89,7 +90,7 @@
flex-direction: column;
margin-bottom: 20px;
padding: 10px 15px;
background: var(--settings-bg-secondary-color);
background: var(--settings-section-bg-color);
border-radius: 12px;
gap: 6px;
}
@@ -190,8 +191,18 @@ details summary {
border: none;
cursor: pointer;
font-weight: 600;
background-color: var(--form-button-bg-color);
color: #fff;
background-color: var(--open-json-button-bg-color);
color: var(--open-json-button-text-color);
}
#open-theme {
padding: 10px 14px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 600;
background-color: var(--open-theme-button-bg-color);
color: var(--open-theme-button-text-color);
}
#experimental-args {
@@ -222,6 +233,33 @@ details summary {
opacity: 1;
}
/* REFRESH THEMES BUTTON */
.refresh-themes-btn {
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--form-input-border-color);
background-color: var(--form-input-bg-color);
color: var(--form-input-text-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.refresh-themes-btn:hover {
background-color: var(--form-button-bg-hover-color);
}
.refresh-themes-btn.spinning svg {
animation: spin 0.6s ease-in-out;
}
.refresh-themes-btn svg {
color: currentColor;
transition: transform 0.6s ease-in-out;
}
/* ANIMATION */
@keyframes popIn {
from { transform: scale(0.95); opacity: 0; }
@@ -232,3 +270,8 @@ details summary {
from { transform: translateX(20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@@ -2,20 +2,12 @@
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0;
z-index: 1100;
height: 48px;
width: 100%;
height: 100%;
background: #1f1f1f98;
color: #eaeaea;
-webkit-app-region: drag;
user-select: none;
}
/* LEFT - custom buttons */

View File

@@ -4,7 +4,7 @@
margin-left: auto;
margin-right: auto;
padding: 1.5em 2em;
background-color: var(--infos-box-color);
background-color: var(--video-info-bg-color);
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
color: var(--video-info-text-color);

View File

@@ -1,29 +1,33 @@
/* Layout */
@import url("layout/form.css");
@import url("layout/settingsPanel.css");
@import url("layout/container.css");
@import url("layout/topbar.css");
@import url("layout/header.css");
@import url("layout/form.css");
@import url("layout/container.css");
@import url("layout/settingsPanel.css");
@import url("layout/videoinfo.css");
/* Components */
@import url("components/checkbox.css");
@import url("components/loader.css");
@import url("theme/themeimport.css");
@import url("components/editpathbutton.css");
@import url("components/progressBar.css");
@import url("components/toast.css");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
#app {
display: block;
opacity: 0;
transition: opacity 0.6s ease;
}
#app.visible {
opacity: 1;
/* Because why not */
#reference{
position: fixed;
pointer-events: none;
bottom:0vh;
left: 10px;
font-size: 1em;
color: #b0b0b02f;
z-index: 9999;
}
html,body{
margin: 0;
padding: 0;
@@ -34,43 +38,45 @@ html,body{
font-style: normal;
}
body {
margin: 0;
body{
background-color: var(--background-color);
width: 100%;
height: 100%;
color: var(--default-text-color);
display: flex;
justify-content: center;
align-items: center;
}
header {
position: fixed;
top: 0;
width: 100%;
z-index: 1100;
height: 48px;
}
main {
width: 100%;
height: calc(100vh - 48px);
margin-top: 48px;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 0;
gap: 0;
}
/* Because why not */
#reference{
position: fixed;
pointer-events: none;
bottom:0vh;
left: 10px;
font-size: 1em;
color: #b0b0b02f;
}
/* Scrollbar globale pour Chromium/Electron */
body::-webkit-scrollbar {
/* Scrollbar pour main */
main::-webkit-scrollbar {
width: 10px;
height: 10px;
}
body::-webkit-scrollbar-track {
main::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 5px;
}
body::-webkit-scrollbar-thumb {
main::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 5px;
border: 2px solid transparent;
@@ -78,36 +84,32 @@ body::-webkit-scrollbar-thumb {
transition: background-color 0.2s ease, transform 0.2s ease;
}
body::-webkit-scrollbar-thumb:hover {
main::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.35);
transform: scaleX(1.05);
}
/* Thème clair */
body.light::-webkit-scrollbar-track {
body.light main::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
}
body.light::-webkit-scrollbar-thumb {
body.light main::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.25);
}
body.light::-webkit-scrollbar-thumb:hover {
body.light main::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.45);
}
.container{
margin-top: 50px;
margin-top: 0px;
}
header p{
#subtitle{
color: var(--subtitle-color);
}
.checkbox-inline{
color: var(--mp3-text-color);
}
body,
.container,
input,

View File

@@ -1,103 +0,0 @@
body.chirac {
background-image: url('../../assets/images/Chirac.png');
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
--background-color: rgba(255, 255, 255, 0.85);
--default-text-color: #002395;
--subtitle-color: #EF4135;
--mp3-text-color: #002395;
/* Formulaire */
--form-bg-color: rgba(255, 255, 255, 0.9);
--form-input-bg-color: #f5f5f5;
--form-input-border-color: #002395;
--form-input-border-focus-color: #EF4135;
--form-input-text-color: #002395;
--form-input-placeholder-color: #999999;
--form-button-bg-color: #002395;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #EF4135;
/* Checkbox */
--checkbox-bg-default: #cccccc;
--checkbox-bg-checked: #002395;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
/* Download status */
--download-status-color: #002395;
/* Video Info Box */
--infos-box-color: rgba(255, 255, 255, 0.9);
--video-info-text-color: #000000;
--video-info-heading-color: #EF4135;
--video-info-list-color: #444444;
--video-info-list-strong-color: #000000;
--video-info-link-color: #002395;
--video-info-link-hover-color: #EF4135;
/* Playlist Color */
--playlist-background-color: #d1d1d1ca;
/* Settings */
--settings-button-bg-color: #00196a;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #0b0528;
--settings-bg-secondary-color: #1c1055;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}
/*
Je serai le président de tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Tous les Français
Mes chers compatriotes
Je serai le président de tous les Français, de tous les Français
Je mesure la difficulté de la tâche qui nous attend
Tous les Français
Tous les Français
Je veux un État, je veux les Français
Je veux un État, je veux
Je veux un État-tat-tat-tat vigoureux
Un État-tat-tat-tat impartial
Un État-tat-tat-tat vigoureux
Un État-tat-tat-tat exigeant
Un État-tat-tat-tat
Je serai le président de tous les Français
Tous les Français
Tous les Français
Tous les Français
Un État qui n'isole pas, tous, pas, pas
Tous, pas, tous, pas, pas
Tous, pas, tous, pas, pas (les Français)
Tous, tous, pas, tous, tous, pas, pas
Tous, tous, pas, tous, tous, pas, pas
Tous, tous, tous, pas, tous, pas, pas (les Français)
Tous, pas, tous, tous, pas, pas
Tous, tous, tous, pas, tous, tous, pas, pas
Tous, tous, pas, tous, tous, pas, pas
Tous, tous, tous-tous, tous-tous, pas, pas
Tous, tous-tous, tous-tous, pas, pas
Un État-tat-tat-tat vigoureux
Un État-tat-tat-tat impartial
Un État-tat-tat-tat exigeant
Un État-tat-tat-tat
Je serai le président de tous les Français
*/

View File

@@ -1,49 +0,0 @@
body.dark {
/* Couleurs générales */
--background-color: #121212; /* noir/gris très foncé */
--default-text-color: #e0e0e0; /* texte clair */
--subtitle-color: #2196f3; /* bleu accent */
--mp3-text-color: #f5f5f5; /* texte checkbox */
/* Formulaire */
--form-bg-color: #1e1e1e; /* fond formulaire */
--form-input-bg-color: #2a2a2a; /* fond inputs */
--form-input-border-color: #444444; /* bordure */
--form-input-border-focus-color: #2196f3; /* focus bleu */
--form-input-text-color: #f5f5f5; /* texte clair */
--form-input-placeholder-color: #888888; /* placeholder gris */
--form-button-bg-color: #2196f3; /* bouton bleu */
--form-button-text-color: #ffffff; /* texte bouton */
--form-button-bg-hover-color: #1976d2; /* hover bleu plus foncé */
/* Checkbox */
--checkbox-bg-default: #555555; /* gris moyen */
--checkbox-bg-checked: #2196f3; /* bleu accent */
--checkbox-checkmark-border-color: #ffffff; /* blanc */
--checkbox-pulse-color: rgba(33, 150, 243, 0.5);
/* Download status */
--download-status-color: #2196f3;
/* Video Info Box */
--infos-box-color: #1a1a1a;
--video-info-text-color: #e0e0e0;
--video-info-heading-color: #2196f3;
--video-info-list-color: #cccccc;
--video-info-list-strong-color: #ffffff;
--video-info-link-color: #2196f3;
--video-info-link-hover-color: #1976d2;
/* Playlist Color */
--playlist-background-color: #303030;
/* Settings */
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #1e1e1e;
--settings-bg-secondary-color: #2a2a2a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
}

View File

@@ -1,61 +0,0 @@
body.drift {
background-image: url('../../assets/images/Drift.jpg');
background-size: cover;
background-position: center;
background-attachment: fixed;
/* Palette principale */
--background-color: #0f1218;
--default-text-color: #3d4b61;
--subtitle-color: #1a1a1a;
--mp3-text-color: #c0d4ff;
/* Formulaire */
--form-bg-color: #1e222a;
--form-input-bg-color: #13171d;
--form-input-border-color: #3b4b6d;
--form-input-border-focus-color: #6ea8ff;
--form-input-text-color: #f0f4ff;
--form-input-placeholder-color: #7a8fae;
--form-button-bg-color: #2a2979;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #353393;
/* Checkbox */
--checkbox-bg-default: #2a2e38;
--checkbox-bg-checked: #2a2979;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(76,108,255,0.3);
/* Download status */
--download-status-color: #ff8e8e;
/* Video Info Box */
--infos-box-color: #1e222ad0;
--video-info-text-color: #e8e8f0;
--video-info-heading-color: #9ab6ff;
--video-info-list-color: #c0d4ff;
--video-info-list-strong-color: #ffffff;
--video-info-link-color: #4c6cff;
--video-info-link-hover-color: #6c8dff;
/* Playlist Color */
--playlist-background-color: #424b5cc2;
/* Settings */
--settings-button-bg-color: #1e222ace;
--settings-button-text-color: #b3b3b4;
--settings-bg-primary-color: #1e222a;
--settings-bg-secondary-color: #272d38;
--settings-text-color: #bababa;
--settings-subtitle-color: #cacaca;
}
body.drift .download-path{
color: #ededed;
}

View File

@@ -1,49 +0,0 @@
body.fanatic{
/* Couleurs générales */
--background-color: #121212;
--default-text-color: #eee;
--subtitle-color: #FF5900;
--mp3-text-color: #ffffff;
/* Formulaire */
--form-bg-color: #1e1e1e;
--form-input-bg-color: #2b2b2b;
--form-input-border-color: #333333;
--form-input-border-focus-color: #FF5900;
--form-input-text-color: #eeeeee;
--form-input-placeholder-color: #666666;
--form-button-bg-color: #FF5900;
--form-button-text-color: #121212;
--form-button-bg-hover-color: #e65500;
/* Checkbox */
--checkbox-bg-default: #555555;
--checkbox-bg-checked: #ff6600;
--checkbox-checkmark-border-color: #eee;
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
/* Download status */
--download-status-color: #FF5900;
/* Video Info Box */
--infos-box-color: #222222;
--video-info-text-color: #eee;
--video-info-heading-color: #FF5900;
--video-info-list-color: #ccc;
--video-info-list-strong-color: #fff;
--video-info-link-color: #FF5900;
--video-info-link-hover-color: #e65500;
/* Playlist Color */
--playlist-background-color: #2b2b2b;
/* Settings */
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #1e1e1e;
--settings-bg-secondary-color: #2d2d2d;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}

View File

@@ -1,48 +0,0 @@
body.light {
/* Couleurs générales */
--background-color: #dadada;
--default-text-color: #222222;
--subtitle-color: #007bff;
--mp3-text-color: #000000;
/* Formulaire */
--form-bg-color: #ffffff;
--form-input-bg-color: #fafafa;
--form-input-border-color: #cccccc;
--form-input-border-focus-color: #007bff;
--form-input-text-color: #333333;
--form-input-placeholder-color: #888888;
--form-button-bg-color: #007bff;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #0056b3;
/* Checkbox */
--checkbox-bg-default: #cccccc;
--checkbox-bg-checked: #007bff;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
/* Download status */
--download-status-color: #007bff;
/* Video Info Box */
--infos-box-color: #ffffff;
--video-info-text-color: #222222;
--video-info-heading-color: #007bff;
--video-info-list-color: #444444;
--video-info-list-strong-color: #000000;
--video-info-link-color: #007bff;
--video-info-link-hover-color: #0056b3;
/* Playlist Color */
--playlist-background-color: #c5c5c5b2;
/* Settings */
--settings-button-bg-color: #e4e4e4;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #cbcbcb;
--settings-bg-secondary-color: #cbcbcb;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #656565;
}

View File

@@ -1,59 +0,0 @@
body.neon {
background: (url("../../assets/images/drift.jpg"));
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
background-size: cover;
background-position: center;
background-attachment: fixed;
/* Couleurs générales */
--background-color: #1b1b2f;
--default-text-color: #e0e0ff;
--subtitle-color: #a3a3ff;
--mp3-text-color: #c0c0ff;
/* Formulaire */
--form-bg-color: #2c2c4a;
--form-input-bg-color: #1f1f3a;
--form-input-border-color: #5858f0;
--form-input-border-focus-color: #a3a3ff;
--form-input-text-color: #ffffff;
--form-input-placeholder-color: #8888ff;
--form-button-bg-color: #5858f0;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #7a7aff;
/* Checkbox */
--checkbox-bg-default: #302b63;
--checkbox-bg-checked: #5858f0;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(88,88,240,0.3);
/* Download status */
--download-status-color: #a3a3ff;
/* Video Info Box */
--infos-box-color: #2c2c4a;
--video-info-text-color: #e0e0ff;
--video-info-heading-color: #a3a3ff;
--video-info-list-color: #c0c0ff;
--video-info-list-strong-color: #ffffff;
--video-info-link-color: #5858f0;
--video-info-link-hover-color: #7a7aff;
/* Playlist Color */
--playlist-background-color: #3e3e64;
/* Settings */
--settings-button-bg-color: #202038;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #242454;
--settings-bg-secondary-color: #2f2f6f;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #d2d2d2;
}
body.neon .download-path {
color: var(--default-text-color);
}

View File

@@ -1,58 +0,0 @@
body.nf {
background-image: url('../../assets/images/NF.jpg');
background-size: cover;
background-position: 70% center;
background-attachment: fixed;
/* Couleurs générales */
--background-color: #1a1a1a;
--default-text-color: #e6dfd5;
--subtitle-color: #f5f0e6;
--mp3-text-color: #d8cfc2;
/* Formulaire */
--form-bg-color: #2a2a2a;
--form-input-bg-color: #222222;
--form-input-border-color: #4a4036;
--form-input-border-focus-color: #cbb89d;
--form-input-text-color: #f0e8dd;
--form-input-placeholder-color: #9a8f80;
--form-button-bg-color: #cbb89d;
--form-button-text-color: #1a1a1a;
--form-button-bg-hover-color: #b7a588;
/* Checkbox */
--checkbox-bg-default: #4a4036;
--checkbox-bg-checked: #cbb89d;
--checkbox-checkmark-border-color: #1a1a1a;
--checkbox-pulse-color: rgba(203,184,157,0.3);
/* Download status */
--download-status-color: #cbb89d;
/* Video Info Box */
--infos-box-color: #2a2a2a;
--video-info-text-color: #e6dfd5;
--video-info-heading-color: #f5f0e6;
--video-info-list-color: #d8cfc2;
--video-info-list-strong-color: #ffffff;
--video-info-link-color: #cbb89d;
--video-info-link-hover-color: #b7a588;
/* Playlist Color */
--playlist-background-color: #4f4f4f;
/* Settings */
--settings-button-bg-color: #2a2a2a;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #2a2a2a;
--settings-bg-secondary-color: #313131;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
body.nf .download-path {
color: var(--default-text-color);
}

View File

@@ -1,58 +0,0 @@
body.songbird {
background-image: url('../../assets/images/Songbird.webp');
background-size: cover;
background-position: center center;
background-attachment: fixed;
/* Couleurs générales */
--background-color: #0a0a0a; /* noir profond */
--default-text-color: #ff2a2a; /* rouge néon */
--subtitle-color: #f2f2f2; /* rouge sombre */
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
/* Formulaire */
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
--form-input-bg-color: #1a0a0a;
--form-input-border-color: #ff2a2a;
--form-input-border-focus-color: #b30000;
--form-input-text-color: #ffffff;
--form-input-placeholder-color: #950404;
--form-button-bg-color: #b30000;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #ff2a2a;
/* Checkbox */
--checkbox-bg-default: #630000;
--checkbox-bg-checked: #ff2a2a;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(255,42,42,0.3);
/* Download status */
--download-status-color: #ff2a2a;
/* Video Info Box */
--infos-box-color: rgba(30,0,0,0.9);
--video-info-text-color: #ff4d4d;
--video-info-heading-color: #ff2a2a;
--video-info-list-color: #b30000;
--video-info-list-strong-color: #ffffff;
--video-info-link-color: #ff2a2a;
--video-info-link-hover-color: #b30000;
/* Playlist Color */
--playlist-background-color: #3f0000;
/* Settings */
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #121212;
--settings-bg-secondary-color: #1a1a1a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
body.cyberpunk .download-path {
color: var(--default-text-color);
}

View File

@@ -1,59 +0,0 @@
body.spicy {
/* Couleurs générales */
--background-color: #121212; /* fond noir profond */
--default-text-color: #ffffff; /* texte clair */
--subtitle-color: #f87918; /* accent rouge piment */
--mp3-text-color: #ffffff; /* texte checkbox inline */
/* Formulaire */
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
--form-input-border-color: #444444; /* bordure gris */
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
--form-input-text-color: #eeeeee; /* texte inputs */
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
--form-button-text-color: #ffffff; /* texte bouton blanc */
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
/* Checkbox */
--checkbox-bg-default: #555555; /* gris moyen */
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
/* Download status */
--download-status-color: #ffffff; /* rouge vif */
/* Video Info Box */
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
--video-info-text-color: #f5f5f5; /* texte clair */
--video-info-heading-color: #ff1a1a; /* titres rouges */
--video-info-list-color: #cccccc; /* texte liste gris clair */
--video-info-list-strong-color: #ffffff; /* texte important blanc */
--video-info-link-color: #ff1a1a; /* liens rouges */
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
/* Playlist Color */
--playlist-background-color: #353232;
/* Settings */
--settings-button-bg-color: #1a1a1a;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #1a1a1a;
--settings-bg-secondary-color: #292929;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}
/* Optionnel: background piments */
body.spicy {
background-image: url('../../assets/images/Spicy.png');
background-size:cover;
background-position: center;
background-attachment: fixed;
background-repeat: no-repeat;
}

View File

@@ -1,9 +0,0 @@
@import url("chirac-theme.css");
@import url("dark-theme.css");
@import url("fanatic-theme.css");
@import url("light-theme.css");
@import url("nf-theme.css");
@import url("drift-theme.css");
@import url("neon-theme.css");
@import url("songbird-theme.css");
@import url("spicy-theme.css")

View File

@@ -3,7 +3,7 @@
--background-color: #001224;
--default-text-color: #eee;
--subtitle-color: #007bff;
--mp3-text-color: #000000;
--audio-only-label-color: #000000;
/* Formulaire */
--form-bg-color: #ffffff;
@@ -15,18 +15,23 @@
--form-button-bg-color: #007bff;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #0056b3;
--paste-button-icon-color: #ffffff;
/* Checkbox */
--checkbox-bg-default: #cccccc;
--checkbox-bg-checked: #3B82F6;
--checkbox-unchecked-bg-color: #cccccc;
--checkbox-checked-bg-color: #3B82F6;
--checkbox-checkmark-border-color: #E0E0E2;
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
/* Download status */
--download-status-color: #007bff;
/* Progress bar */
--progress-bar-bg-color: #cccccc;
--progress-bar-fill-color: #007bff;
/* Video Info Box */
--infos-box-color: #f0f0f0;
--video-info-bg-color: #f0f0f0;
--video-info-text-color: #222222;
--video-info-heading-color: #007bff;
--video-info-list-color: #444444;
@@ -34,12 +39,15 @@
--video-info-link-color: #007bff;
--video-info-link-hover-color: #0056b3;
/* Playlist */
--playlist-background-color: #4f4f4f;
/* Settings */
--settings-button-bg-color: var(--form-bg-color);
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: var(--form-bg-color);
--settings-bg-secondary-color: var(--form-bg-color);
--settings-modal-bg-color: var(--form-bg-color);
--settings-section-bg-color: var(--form-bg-color);
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;

View File

@@ -1,10 +1,13 @@
const { fetchDownload } = require("../services/download.services");
const { fetchDownload, cancelDownload } = require("../services/download.services");
const { logger } = require("../logger");
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
const { configFeatures } = require("../../config");
const listeners = [];
const speedListeners = [];
const stageListeners = [];
const playlistInfoListeners = [];
async function downloadController(req, res) {
try {
@@ -13,22 +16,23 @@ async function downloadController(req, res) {
audioOnly: req.body.audioOnly === "1",
quality: req.body.quality || "best",
outputFolder: req.body.savePath || undefined,
playlistTitle: req.body.playlistTitle || undefined,
};
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
if (options.outputFolder && !isSafePath(options.outputFolder)) {
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
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(options, listeners, speedListeners);
notifyDownloadFinished(outputFolder);
res.send("Download Done !");
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
res.send("Download Done !");
} catch (err) {
logger.error(`Server Error in /download : ${err.message}`);
res.status(500).send(`Server Error`);
res.status(500).send(`Server Error`);
}
}
@@ -64,5 +68,47 @@ function speedController(req, res) {
});
}
function stageController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
module.exports = { downloadController, progressController, speedController};
const sendStage = stage => res.write(`data: ${stage}\n\n`);
stageListeners.push(sendStage);
req.on('close', () => {
stageListeners.splice(stageListeners.indexOf(sendStage), 1);
res.end();
});
}
function cancelDownloadController(req, res) {
const cancelled = cancelDownload();
if (cancelled) {
logger.info("Download and queue cancelled by user");
res.send("Download stopped! Queue cleared.");
} else {
logger.warn("No download to cancel");
res.status(400).send("No download in progress !");
}
}
function playlistInfoController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
playlistInfoListeners.push(sendPlaylistInfo);
req.on('close', () => {
playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1);
res.end();
});
}
module.exports = { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController};

View File

@@ -10,7 +10,7 @@ async function infoController(req, res){
// If no url, non-string url or invalid url
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
@@ -22,7 +22,32 @@ async function infoController(req, res){
}
try {
const data = await fetchInfo(url);
let data;
let fetchError = null;
try {
data = await fetchInfo(url);
} catch (err) {
fetchError = err;
// If it's a playlist URL and fetch failed, try fetching just the single video
if ((url.includes("&list") || url.includes("?list"))) {
logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`);
// Remove the &list parameter
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
try {
data = await fetchInfo(singleVideoUrl);
logger.info(`Single video fetch succeeded after playlist failure`);
} catch (retryErr) {
logger.error(`Single video fetch also failed: ${retryErr.message}`);
throw err; // Throw the original error
}
} else {
throw err;
}
}
if (data._type === "playlist") {
@@ -39,7 +64,7 @@ async function infoController(req, res){
} catch (err) {
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
return res.status(500).send(`Unable to fetch info`);
return res.status(500).send(`Unable to fetch info`);
}
}

View File

@@ -1,7 +1,9 @@
const { Notification, shell } = require("electron");
const { iconPaths } = require("./path.helpers");
function notifyDownloadFinished(folder) {
function notifyDownloadFinished(folder, notifyEnabled = true) {
if (!notifyEnabled) return;
const notif = new Notification({
title: "Freedom Loader",
body: "Your download is complete, click here to open it.",

View File

@@ -50,8 +50,74 @@ const binaryPaths = {
deno: path.join(resourcesPath, "binaries", "deno.exe")
};
// Theme paths - themes stored in AppData to survive updates
const userThemesPath = path.join(app.getPath("userData"), "themes");
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
function initUserThemes() {
try {
if (!fs.existsSync(userThemesPath)) {
fs.mkdirSync(userThemesPath, { recursive: true });
// Ensure folder is writable
fs.chmodSync(userThemesPath, 0o777);
logger.info("Created user themes directory");
}
// Copy default themes from resources to user directory if they don't exist
if (fs.existsSync(defaultThemesSourcePath)) {
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
for (const theme of defaultThemes) {
const srcPath = path.join(defaultThemesSourcePath, theme);
const destPath = path.join(userThemesPath, theme);
if (!fs.existsSync(destPath)) {
try {
copyDirectory(srcPath, destPath);
fs.chmodSync(destPath, 0o777);
logger.info(`Copied default theme: ${theme}`);
} catch (copyErr) {
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
try {
fs.mkdirSync(destPath, { recursive: true });
fs.chmodSync(destPath, 0o777);
} catch (mkdirErr) {
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.message}`);
}
}
}
}
} else {
logger.warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
}
} catch (err) {
logger.error(`Failed to initialize user themes: ${err.message}`);
}
}
function copyDirectory(src, dest) {
try {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true, mode: 0o777 });
}
const files = fs.readdirSync(src);
for (const file of files) {
const srcFile = path.join(src, file);
const destFile = path.join(dest, file);
if (fs.statSync(srcFile).isDirectory()) {
copyDirectory(srcFile, destFile);
} else {
fs.copyFileSync(srcFile, destFile);
}
}
} catch (err) {
throw new Error(`Copy error: ${err.message}`);
}
}
if (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")}
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder };
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };

View File

@@ -1,11 +1,14 @@
const express = require("express");
const router = express.Router();
const { downloadController, progressController, speedController } = require("../controller/download.controller");
const { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController } = require("../controller/download.controller");
router.post("/", downloadController);
router.post("/cancel", cancelDownloadController);
// SSE for download progress
router.get("/progress", progressController);
router.get("/speed", speedController);
router.get("/stage", stageController);
router.get("/playlist-info", playlistInfoController);
module.exports = router;

View File

@@ -2,30 +2,19 @@ const express = require("express");
const path = require("path");
const { logger, logSessionEnd } = require("./logger");
const config = require("../config");
const { execFile } = require("child_process");
const { userYtDlp } = require("./helpers/path.helpers");
const { rateLimite } = require("./helpers/rateLimit.helpers")
const { rateLimite } = require("./helpers/rateLimit.helpers");
const app = express();
app.use(express.json());
// Update YT-DLP on Startup
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
if (err) logger.warn("yt-dlp update error:", err);
else logger.info(`Update yt-dlp : ${stdout}`);
});
// Middlewares
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, "../public")));
// Routes
app.use("/download", require("./routes/download.route"));
app.use("/info", require("./routes/info.route"));
// When we get API Root, it return the front pages.
app.get("/", rateLimite ,(req, res) => {
app.get("/", rateLimite, (req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html"));
});
@@ -37,14 +26,14 @@ async function startServer() {
});
server.on("error", (err) => {
logger.error("Express Server Error :", err);
logger.error("Express server error:", err);
reject(err);
});
const gracefulExit = () => {
logSessionEnd();
server.close(() => {
logger.info("Express Server close cleanly.");
logger.info("Express server closed cleanly.");
process.exit();
});
};

View File

@@ -6,11 +6,80 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
const notify = require("../helpers/notify.helpers");
const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers");
const { configFeatures } = require("../../config.js");
function fetchDownload(options, listeners, speedListeners) {
let currentDownloadProcess = null;
// Détecte si une URL est une playlist
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 200);
const playlistPath = path.join(basePath, sanitizedTitle);
try {
if (!fs.existsSync(playlistPath)) {
fs.mkdirSync(playlistPath, { recursive: true });
logger.info(`Playlist folder created: ${playlistPath}`);
return playlistPath;
}
let counter = 1;
let newPath;
while (counter <= 1000) {
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
if (!fs.existsSync(newPath)) {
fs.mkdirSync(newPath, { recursive: true });
logger.info(`Playlist folder created with increment: ${newPath}`);
return newPath;
}
counter++;
}
logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
} catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
// Fallback: create folder "Untitled Playlist X"
let counter = 1;
let fallbackPath;
while (counter <= 1000) {
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
if (!fs.existsSync(fallbackPath)) {
try {
fs.mkdirSync(fallbackPath, { recursive: true });
logger.info(`Fallback playlist folder created: ${fallbackPath}`);
return fallbackPath;
} catch (mkErr) {
counter++;
continue;
}
}
counter++;
}
logger.error(`Could not create playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
}
}
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
return new Promise((resolve, reject) => {
const outputFolder = options.outputFolder || defaultDownloadFolder;
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
let outputFolder = options.outputFolder || defaultDownloadFolder;
// Normalize path and validate it's safe (within Users folder)
let safeOutputFolder = path.resolve(outputFolder);
@@ -29,16 +98,35 @@ function fetchDownload(options, listeners, speedListeners) {
return reject(new Error(`Unable to create download folder: ${err.message}`));
}
// Détecte si c'est une playlist et crée un dossier approprié
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
if (isPlaylist && configFeatures.createPlaylistFolders) {
try {
const playlistName = options.playlistTitle || "Untitled Playlist";
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
logger.info(`Playlist detected, using folder: ${safeOutputFolder}`);
} catch (err) {
logger.error(`Failed to create playlist folder: ${err.message}`);
return reject(err);
}
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
}
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
logger.info(`[yt-dlp args] ${args.join(" ")}`);
const child = execFile(userYtDlp, args);
currentDownloadProcess = child;
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
child.on("close", code => {
currentDownloadProcess = null;
listeners.forEach(fn => fn("done"));
if (code === 0) resolve(safeOutputFolder);
else if (code === null) reject(new Error(`Download cancelled by user`));
else reject(new Error(`YT-DLP failed with code : ${code}`));
});
@@ -49,7 +137,10 @@ function fetchDownload(options, listeners, speedListeners) {
logger.info(`[yt-dlp] ${line}`);
// Progress Bar
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
if (line.startsWith("[download] Destination:")) {
listeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading..."));
}
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
@@ -61,6 +152,30 @@ function fetchDownload(options, listeners, speedListeners) {
}
}
// Playlist item tracking
const itemMatch = line.match(/Downloading\s+(?:item|video)?\s*(\d+)\s+of\s+(\d+)/i);
if (itemMatch) {
const itemInfo = `Item ${itemMatch[1]} of ${itemMatch[2]}`;
playlistInfoListeners.forEach(fn => fn(itemInfo));
}
// Stage messages
if (line.includes("[Merger]")) {
stageListeners.forEach(fn => fn("Merging formats..."));
}
if (line.includes("[ExtractAudio]")) {
stageListeners.forEach(fn => fn("Extracting audio..."));
}
if (line.includes("[Metadata]")) {
stageListeners.forEach(fn => fn("Adding metadata..."));
}
if (line.includes("[ThumbnailsConvertor]")) {
stageListeners.forEach(fn => fn("Converting thumbnail..."));
}
if (line.includes("[EmbedThumbnail]")) {
stageListeners.forEach(fn => fn("Embedding thumbnail..."));
}
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
notify.notifyCookiesBrowserError();
}
@@ -76,4 +191,31 @@ function fetchDownload(options, listeners, speedListeners) {
});
}
module.exports = { fetchDownload };
function cancelDownload() {
if (currentDownloadProcess) {
logger.info("Cancelling download and killing all child processes...");
// Force kill the process and all its children with SIGKILL
try {
// Try to kill with SIGKILL on Windows (process group) or Unix
if (process.platform === 'win32') {
// On Windows, kill the process tree
require('child_process').execSync(`taskkill /PID ${currentDownloadProcess.pid} /T /F`, { stdio: 'ignore' });
} else {
// On Unix, send SIGKILL to process group
process.kill(-currentDownloadProcess.pid, 'SIGKILL');
}
} catch (err) {
logger.warn(`Error killing process: ${err.message}`);
// Fallback to regular kill
currentDownloadProcess.kill('SIGKILL');
}
currentDownloadProcess = null;
logger.info("Download cancelled successfully");
return true;
}
return false;
}
module.exports = { fetchDownload, cancelDownload };

View File

@@ -1,47 +0,0 @@
const { autoUpdater } = require("electron-updater");
const { app, Notification, shell} = require("electron");
const { logger } = require("./logger");
function AutoUpdater() {
autoUpdater.on("update-available", (info) => {
logger.info(`New Version Available : ${info.version}`);
new Notification({
title: "Freedom Loader",
body: `New Version Available : ${info.version}. Application will restart`
}).show();
});
autoUpdater.on("update-downloaded", (info) => {
logger.info(`Update Downloaded : ${info.version}`);
new Notification({
title: "Freedom Loader",
body: `Update ${info.version} downloaded.`
}).on("click", () =>
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
).show();
setTimeout(() => {
autoUpdater.quitAndInstall();
}, 5000);
});
autoUpdater.on("error", (err) => {
logger.error("Auto Update Error :", err.message);
new Notification({
title: "Freedom Loader - Update Error",
body: err.message
}).show();
});
app.whenReady().then(async () => {
try {
await autoUpdater.checkForUpdates();
logger.info("Update check completed");
} catch (err) {
logger.error("Error during update check :", err.message);
}
});
}
module.exports = { AutoUpdater };

96
theme/template.theme.json Normal file
View File

@@ -0,0 +1,96 @@
{
"meta": {
"name": "",
"author": "",
"version": "1.0.0",
"formatVersion": "1.0"
},
"style": {
"background": {
"image": "",
"size": "cover",
"position": "center",
"attachment": "fixed"
},
"colors": {
"background": "",
"text": {
"default": "",
"subtitle": "",
"audioOnly": ""
}
},
"form": {
"background": "",
"input": {
"background": "",
"border": "",
"borderFocus": "",
"text": "",
"placeholder": ""
},
"button": {
"background": "",
"text": "",
"hover": ""
},
"pasteButtonIcon": ""
},
"checkbox": {
"background": {
"unchecked": "",
"checked": ""
},
"checkmarkBorder": "",
"pulse": ""
},
"download": {
"status": ""
},
"progressBar": {
"background": "",
"fill": ""
},
"videoInfo": {
"background": "",
"text": "",
"heading": "",
"list": "",
"strong": "",
"link": "",
"linkHover": ""
},
"playlist": {
"background": ""
},
"settings": {
"button": {
"background": "",
"text": ""
},
"openThemeButton": {
"background": "",
"text": ""
},
"openJsonButton": {
"background": "",
"text": ""
},
"background": {
"modal": "",
"section": ""
},
"text": "",
"subtitle": ""
}
}
}