diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 436818d..449cf5b 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -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
diff --git a/.gitignore b/.gitignore
index b044080..825f0c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,9 @@
/ressources/ffprobe.exe
/ressources/deno.exe
+/theme/**
+
+config/config.dev.json
+
logs/*.json
logs/*.log
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8078a7e..fca3c0f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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)
---
diff --git a/README.md b/README.md
index 25dc446..c266f3d 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,17 @@
-
### **A clean, open-source multimedia downloader for Windows**
-[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
+[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[](https://www.gnu.org/licenses/gpl-3.0)
-[](https://masteracnolo.github.io/FreedomLoader/)
-
+[](https://masteracnolo.github.io/Freedom-Loader-Site/)
+[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
+
@@ -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)
\ No newline at end of file
diff --git a/app/autoUpdater.js b/app/autoUpdater.js
new file mode 100644
index 0000000..548020d
--- /dev/null
+++ b/app/autoUpdater.js
@@ -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 };
\ No newline at end of file
diff --git a/app/dependencyCheck.js b/app/dependencyCheck.js
new file mode 100644
index 0000000..ba285c9
--- /dev/null
+++ b/app/dependencyCheck.js
@@ -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 };
\ No newline at end of file
diff --git a/server/discordRPC.js b/app/discordRPC.js
similarity index 95%
rename from server/discordRPC.js
rename to app/discordRPC.js
index 90c96d4..49b5416 100644
--- a/server/discordRPC.js
+++ b/app/discordRPC.js
@@ -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" });
diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js
new file mode 100644
index 0000000..611dc91
--- /dev/null
+++ b/app/ipcHandlers.js
@@ -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 };
\ No newline at end of file
diff --git a/app/pathValidator.js b/app/pathValidator.js
new file mode 100644
index 0000000..aa69107
--- /dev/null
+++ b/app/pathValidator.js
@@ -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 };
\ No newline at end of file
diff --git a/app/splashManager.js b/app/splashManager.js
new file mode 100644
index 0000000..acb5b37
--- /dev/null
+++ b/app/splashManager.js
@@ -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 };
\ No newline at end of file
diff --git a/app/themeManager.js b/app/themeManager.js
new file mode 100644
index 0000000..9a997ee
--- /dev/null
+++ b/app/themeManager.js
@@ -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 };
\ No newline at end of file
diff --git a/app/windowManager.js b/app/windowManager.js
new file mode 100644
index 0000000..3b42bc7
--- /dev/null
+++ b/app/windowManager.js
@@ -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 };
\ No newline at end of file
diff --git a/app/ytDlpUpdater.js b/app/ytDlpUpdater.js
new file mode 100644
index 0000000..90b400b
--- /dev/null
+++ b/app/ytDlpUpdater.js
@@ -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 };
\ No newline at end of file
diff --git a/config.js b/config.js
index 4a40e97..3e8a43d 100644
--- a/config.js
+++ b/config.js
@@ -5,23 +5,39 @@ 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;
+ const raw = fs.readFileSync(featuresPath, "utf-8");
+ return JSON.parse(raw);
}
const configFeatures = loadFeatures();
module.exports = {
- version: packageJson.version,
- applicationPort: "8787",
- localMode,
- DiscordRPCID: "1410934537051181146",
- configFeatures
+ version: packageJson.version,
+ applicationPort: "8787",
+ localMode,
+ DiscordRPCID: "1410934537051181146",
+ configFeatures,
+ featuresPath
}
\ No newline at end of file
diff --git a/config/config.json b/config/config.default.json
similarity index 79%
rename from config/config.json
rename to config/config.default.json
index d73ef86..7b68c4c 100644
--- a/config/config.json
+++ b/config/config.default.json
@@ -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"
}
\ No newline at end of file
diff --git a/main.js b/main.js
index 58621c9..829a519 100644
--- a/main.js
+++ b/main.js
@@ -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();
+if (!config.localMode) {
+ const gotLock = app.requestSingleInstanceLock();
+ if (gotLock) {
+ app.on("second-instance", () => {
+ logger.info("New instance detected, closing older...");
+ getMainWindow()?.destroy();
});
- 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 {
- app.on("second-instance", () => {
- // La vieille instance se ferme
- if (mainWindow) {
- logger.info("New Instance Detected, closing the older...");
- mainWindow.destroy();
- mainWindow = null;
- }
- });
- }
- }
-}
-// 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!")
- logSessionEnd();
-});
+ logger.info("All services stopped. Have a nice day!");
+ logSessionEnd();
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f2d2963..ed318fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "freedom-loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freedom-loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
@@ -14,6 +14,7 @@
"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"
@@ -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",
@@ -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,6 +3811,15 @@
"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.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -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",
@@ -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",
@@ -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",
diff --git a/package.json b/package.json
index 25d9ebc..9fcef47 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "freedom-loader",
"productName": "Freedom Loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"author": "MasterAcnolo",
"description": "Freedom Loader",
"main": "main.js",
@@ -17,6 +17,7 @@
"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"
@@ -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": {
diff --git a/preload.js b/preload.js
index e9ca875..a136cc5 100644
--- a/preload.js
+++ b/preload.js
@@ -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")
});
diff --git a/public/assets/images/Chirac.png b/public/assets/images/Chirac.png
deleted file mode 100644
index f40b8e7..0000000
Binary files a/public/assets/images/Chirac.png and /dev/null differ
diff --git a/public/assets/images/Drift.jpg b/public/assets/images/Drift.jpg
deleted file mode 100644
index 9fe9e29..0000000
Binary files a/public/assets/images/Drift.jpg and /dev/null differ
diff --git a/public/assets/images/NF.jpg b/public/assets/images/NF.jpg
deleted file mode 100644
index f8d4f55..0000000
Binary files a/public/assets/images/NF.jpg and /dev/null differ
diff --git a/public/assets/images/Songbird.webp b/public/assets/images/Songbird.webp
deleted file mode 100644
index e43a4b8..0000000
Binary files a/public/assets/images/Songbird.webp and /dev/null differ
diff --git a/public/assets/images/Spicy.png b/public/assets/images/Spicy.png
deleted file mode 100644
index c190be6..0000000
Binary files a/public/assets/images/Spicy.png and /dev/null differ
diff --git a/public/index.html b/public/index.html
index 40970a4..c684b4a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,7 +9,6 @@
font-src https://fonts.gstatic.com;
img-src 'self' data: https:;
">
-
@@ -17,20 +16,24 @@
-
-
-
Tools
-
Logs
-
Website
-
Wiki
+
-
+
+
ionicons-v5-q
@@ -52,12 +55,18 @@
-
+
Choose the theme of the application
@@ -80,20 +89,20 @@
-
-
-
-
-
- Custom Top bar
- Use this if the topbar is missing or you prefer a custom one.
-
@@ -142,6 +151,14 @@
+
+
+
+ Custom Top bar
+ Use this if the topbar is missing or you prefer a custom one.
+
+
+
Custom Codec
@@ -191,6 +208,7 @@
@@ -198,78 +216,88 @@
-
+
+
+
Freedom Loader
+
Because why not?
+
+
+
+
+
+
+ Will be save on
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
Fetching info...
+
+
+
+
+
+
-
- Freedom Loader
- Because why not?
-
-
-
-
-
-
-
-
-
- Audio Only (MP3)
-
-
- Best Quality
- Medium Quality
- Lowest Quality
- 1080p
- 720p
- 480p
-
-
-
Download
-
-
-
-
- Will be save on
- Edit
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Fetching info...
-
-
-
-
-
-
+
+
Because Why Not ?
+
diff --git a/public/script/custompath.js b/public/script/custompath.js
index e530c1e..a0994ce 100644
--- a/public/script/custompath.js
+++ b/public/script/custompath.js
@@ -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
}
diff --git a/public/script/customthemes.js b/public/script/customthemes.js
index 7e5262c..c9649ac 100644
--- a/public/script/customthemes.js
+++ b/public/script/customthemes.js
@@ -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();
\ No newline at end of file
+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();
\ No newline at end of file
diff --git a/public/script/downloadstatus.js b/public/script/downloadstatus.js
index e6818b0..b6362ec 100644
--- a/public/script/downloadstatus.js
+++ b/public/script/downloadstatus.js
@@ -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";
}
-});
\ No newline at end of file
+});
+
+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";
+}
\ No newline at end of file
diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js
index 1fc1f75..e992282 100644
--- a/public/script/fetchinfo.js
+++ b/public/script/fetchinfo.js
@@ -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 = `