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 @@
- Banner ### **A clean, open-source multimedia downloader for Windows** -[![Release](https://img.shields.io/badge/Release-1.4.7-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/) - +[![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/) +
@@ -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 @@ -
-
- - - - +
+
+
+ + + + + + +
+
+ + + +
-
- - - -
-
- + + @@ -52,12 +55,18 @@
-
+
Thème +
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 + +
+ +
+ +
+
+
+
+
+
+ + +
+
+ + + +
+ + + +
+
+ + -
-

Freedom Loader

-

Because why not?

-
- -
-
- -
+
- - - - - -
- -
- Will be save on - -
- -
-
-
- -
-
-
-
- -
- -
- - - -
-
- - + +

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 = `
@@ -194,6 +208,7 @@ async function init() { } infoDiv.classList.remove("playlist-mode"); + playlistTitleInput.value = ""; // ---------- VIDEO NORMALE ---------- const durationStr = data.duration diff --git a/public/script/progressBar.js b/public/script/progressBar.js index 780a551..4b8e5ce 100644 --- a/public/script/progressBar.js +++ b/public/script/progressBar.js @@ -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") { - resetProgress(); - window.electronAPI.setProgress(-1); + // 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,14 +78,21 @@ 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 } }; 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" }; \ No newline at end of file diff --git a/public/script/settingsPanel.js b/public/script/settingsPanel.js index 50fd402..b5a1168 100644 --- a/public/script/settingsPanel.js +++ b/public/script/settingsPanel.js @@ -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); }); diff --git a/public/script/toast.js b/public/script/toast.js new file mode 100644 index 0000000..0cb3822 --- /dev/null +++ b/public/script/toast.js @@ -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 = ` + ${icon} + ${message} + + `; + + // 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); diff --git a/public/script/topbar.js b/public/script/topbar.js index be7f584..a13e000 100644 --- a/public/script/topbar.js +++ b/public/script/topbar.js @@ -1,29 +1,28 @@ // 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'); - const devtoolsBtn = document.getElementById('devtools-btn'); - const logsBtn = document.getElementById('logs-btn'); - const websiteBtn = document.getElementById('website-btn'); - const wikiBtn = document.getElementById('wiki-btn'); - const configBtn = document.getElementById('config-btn'); + const { topbarAPI } = window; + if (!topbarAPI) return; + + const minBtn = document.getElementById('minimize-btn'); + const maxBtn = document.getElementById('maximize-btn'); + const closeBtn = document.getElementById('close-btn'); + const devtoolsBtn = document.getElementById('devtools-btn'); + const logsBtn = document.getElementById('logs-btn'); + const websiteBtn = document.getElementById('website-btn'); + const wikiBtn = document.getElementById('wiki-btn'); + const workshopBtn = document.getElementById('workshop-btn'); - if (minBtn) minBtn.onclick = () => topbarAPI.minimize(); - if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize(); - if (closeBtn) closeBtn.onclick = () => topbarAPI.close(); - if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools(); - 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 (minBtn) minBtn.onclick = () => topbarAPI.minimize(); + if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize(); + if (closeBtn) closeBtn.onclick = () => topbarAPI.close(); + if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools(); + if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs(); + if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite(); + if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki(); + 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(); diff --git a/public/splash.html b/public/splash.html new file mode 100644 index 0000000..93b4649 --- /dev/null +++ b/public/splash.html @@ -0,0 +1,94 @@ + + + + + + + + +
+ Freedom Loader +
+
+
+
+ Starting... +
+
+ + + + + \ No newline at end of file diff --git a/public/styles/components/checkbox.css b/public/styles/components/checkbox.css index bfb8fc4..693b2b5 100644 --- a/public/styles/components/checkbox.css +++ b/public/styles/components/checkbox.css @@ -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; } diff --git a/public/styles/components/editpathbutton.css b/public/styles/components/editpathbutton.css index 3c00a38..aab8387 100644 --- a/public/styles/components/editpathbutton.css +++ b/public/styles/components/editpathbutton.css @@ -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; } diff --git a/public/styles/components/loader.css b/public/styles/components/loader.css index 812b5a3..eca3caa 100644 --- a/public/styles/components/loader.css +++ b/public/styles/components/loader.css @@ -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; } diff --git a/public/styles/components/progressBar.css b/public/styles/components/progressBar.css index 7a38726..17e0ea2 100644 --- a/public/styles/components/progressBar.css +++ b/public/styles/components/progressBar.css @@ -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; +} diff --git a/public/styles/components/toast.css b/public/styles/components/toast.css new file mode 100644 index 0000000..5972c0b --- /dev/null +++ b/public/styles/components/toast.css @@ -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; + } +} diff --git a/public/styles/layout/container.css b/public/styles/layout/container.css index 14e1ad4..06cd686 100644 --- a/public/styles/layout/container.css +++ b/public/styles/layout/container.css @@ -4,7 +4,6 @@ left: 0; right: 0; padding: 1em; - z-index: 1000; display: flex; flex-direction: column; align-items: center; diff --git a/public/styles/layout/form.css b/public/styles/layout/form.css index 3a540bc..4ef5349 100644 --- a/public/styles/layout/form.css +++ b/public/styles/layout/form.css @@ -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; } diff --git a/public/styles/layout/header.css b/public/styles/layout/header.css index f2ba12f..76f7f62 100644 --- a/public/styles/layout/header.css +++ b/public/styles/layout/header.css @@ -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; + align-items: center; } - -header h1{ - margin-bottom: 0.2vw; +#title { + color: var(--default-text-color); } \ No newline at end of file diff --git a/public/styles/layout/settingsPanel.css b/public/styles/layout/settingsPanel.css index 0fa7b3d..32b81e0 100644 --- a/public/styles/layout/settingsPanel.css +++ b/public/styles/layout/settingsPanel.css @@ -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; } @@ -231,4 +269,9 @@ details summary { @keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } \ No newline at end of file diff --git a/public/styles/layout/topbar.css b/public/styles/layout/topbar.css index 939b931..158d8d2 100644 --- a/public/styles/layout/topbar.css +++ b/public/styles/layout/topbar.css @@ -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 */ diff --git a/public/styles/layout/videoinfo.css b/public/styles/layout/videoinfo.css index 79ecb98..f897421 100644 --- a/public/styles/layout/videoinfo.css +++ b/public/styles/layout/videoinfo.css @@ -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); diff --git a/public/styles/styles.css b/public/styles/styles.css index 2a4aa3e..db0db99 100644 --- a/public/styles/styles.css +++ b/public/styles/styles.css @@ -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; - background-color: var(--background-color); +body{ + background-color: var(--background-color); + color: var(--default-text-color); +} + +header { + position: fixed; + top: 0; width: 100%; - height: 100%; - color: var(--default-text-color); - display: flex; - justify-content: center; - align-items: center; + 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, diff --git a/public/styles/theme/chirac-theme.css b/public/styles/theme/chirac-theme.css deleted file mode 100644 index 2645be5..0000000 --- a/public/styles/theme/chirac-theme.css +++ /dev/null @@ -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 - -*/ \ No newline at end of file diff --git a/public/styles/theme/dark-theme.css b/public/styles/theme/dark-theme.css deleted file mode 100644 index e1bd48f..0000000 --- a/public/styles/theme/dark-theme.css +++ /dev/null @@ -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; - -} diff --git a/public/styles/theme/drift-theme.css b/public/styles/theme/drift-theme.css deleted file mode 100644 index f55f365..0000000 --- a/public/styles/theme/drift-theme.css +++ /dev/null @@ -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; -} diff --git a/public/styles/theme/fanatic-theme.css b/public/styles/theme/fanatic-theme.css deleted file mode 100644 index 401fd67..0000000 --- a/public/styles/theme/fanatic-theme.css +++ /dev/null @@ -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; -} diff --git a/public/styles/theme/light-theme.css b/public/styles/theme/light-theme.css deleted file mode 100644 index 4b1d2b1..0000000 --- a/public/styles/theme/light-theme.css +++ /dev/null @@ -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; -} diff --git a/public/styles/theme/neon-theme.css b/public/styles/theme/neon-theme.css deleted file mode 100644 index 63c87da..0000000 --- a/public/styles/theme/neon-theme.css +++ /dev/null @@ -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); -} diff --git a/public/styles/theme/nf-theme.css b/public/styles/theme/nf-theme.css deleted file mode 100644 index 5b7ac42..0000000 --- a/public/styles/theme/nf-theme.css +++ /dev/null @@ -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); -} diff --git a/public/styles/theme/songbird-theme.css b/public/styles/theme/songbird-theme.css deleted file mode 100644 index 976d0cc..0000000 --- a/public/styles/theme/songbird-theme.css +++ /dev/null @@ -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); -} diff --git a/public/styles/theme/spicy-theme.css b/public/styles/theme/spicy-theme.css deleted file mode 100644 index a001407..0000000 --- a/public/styles/theme/spicy-theme.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/public/styles/theme/themeimport.css b/public/styles/theme/themeimport.css deleted file mode 100644 index 7de5d07..0000000 --- a/public/styles/theme/themeimport.css +++ /dev/null @@ -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") \ No newline at end of file diff --git a/public/styles/variables.css b/public/styles/variables.css index 29362d3..a47550e 100644 --- a/public/styles/variables.css +++ b/public/styles/variables.css @@ -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; diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js index fe652ab..edb0e81 100644 --- a/server/controller/download.controller.js +++ b/server/controller/download.controller.js @@ -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}; diff --git a/server/controller/info.controller.js b/server/controller/info.controller.js index f9b0fe9..57f42b3 100644 --- a/server/controller/info.controller.js +++ b/server/controller/info.controller.js @@ -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`); } } diff --git a/server/helpers/notify.helpers.js b/server/helpers/notify.helpers.js index 16268b1..20ef59e 100644 --- a/server/helpers/notify.helpers.js +++ b/server/helpers/notify.helpers.js @@ -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.", diff --git a/server/helpers/path.helpers.js b/server/helpers/path.helpers.js index ed630c4..38efa9e 100644 --- a/server/helpers/path.helpers.js +++ b/server/helpers/path.helpers.js @@ -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 }; diff --git a/server/routes/download.route.js b/server/routes/download.route.js index 14a9f3f..1d814f6 100644 --- a/server/routes/download.route.js +++ b/server/routes/download.route.js @@ -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; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 3992fd9..a31e6ec 100644 --- a/server/server.js +++ b/server/server.js @@ -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.use("/info", require("./routes/info.route")); +app.get("/", rateLimite, (req, res) => { res.sendFile(path.join(__dirname, "../public/index.html")); }); @@ -37,19 +26,19 @@ 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(); }); }; - process.on("SIGINT", gracefulExit); + process.on("SIGINT", gracefulExit); process.on("SIGTERM", gracefulExit); }); } diff --git a/server/services/download.services.js b/server/services/download.services.js index 4ff58e7..b80d176 100644 --- a/server/services/download.services.js +++ b/server/services/download.services.js @@ -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 }; diff --git a/server/update.js b/server/update.js deleted file mode 100644 index d38aa70..0000000 --- a/server/update.js +++ /dev/null @@ -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 }; diff --git a/theme/template.theme.json b/theme/template.theme.json new file mode 100644 index 0000000..ab655f3 --- /dev/null +++ b/theme/template.theme.json @@ -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": "" + } + } +} \ No newline at end of file