diff --git a/README-EN.md b/README-EN.md index a80b211..b888e7b 100644 --- a/README-EN.md +++ b/README-EN.md @@ -1,5 +1,8 @@ # Freedom Loader + +![Static Badge](https://img.shields.io/badge/Build-1.1.1--beta-yellow?style=flat&logo=Test) + ![Static Badge](https://img.shields.io/badge/Latest%20Release_-1.0.1-blue?style=flat) ![Static Badge](https://img.shields.io/badge/Build-1.0.2%20Beta-yellow?style=flat) > This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content. Eventually, it will allow users to choose @@ -11,6 +14,8 @@ ✅ Detailed Logs (Console + Logs) ✅ Automatic Fetch of Information ✅ Playlist Download +✅ Theme selection +✅ Custom output path selection for videos --- diff --git a/README.md b/README.md index 275c4eb..bf033c7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Freedom Loader + +![Static Badge](https://img.shields.io/badge/Build-1.1.1--beta-yellow?style=flat&logo=Test) + ![Static Badge](https://img.shields.io/badge/Latest%20Release_-1.0.1-blue?style=flat) ![Static Badge](https://img.shields.io/badge/Build-1.0.2%20Beta-yellow?style=flat) > Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir @@ -11,6 +14,8 @@ ✅ Logs détaillés (console + fichiers logs) ✅ Récupération automatique des informations ✅ Téléchargement des playlists +✅ Choix de thèmes +✅ Choix du chemin de sortie des vidéos personnalisés --- diff --git a/main.js b/main.js index af0e2b8..6017925 100644 --- a/main.js +++ b/main.js @@ -16,19 +16,26 @@ along with this program. If not, see . */ -const { app, BrowserWindow } = require("electron"); +const { app, BrowserWindow, ipcMain, dialog } = require("electron"); const path = require("path"); +const os = require("os"); const { logger, logSessionStart, logSessionEnd } = require("./server/logger"); let mainWindow; +/* + Fonction principale qui crée la fenêtre principale de l'application. + Elle évite la création multiple et configure les dimensions et options de la fenêtre. + Charge l'URL locale du serveur Express et gère les erreurs éventuelles. +*/ async function createWindow() { - logger.info("Creation de la fenetre..."); - + logger.info("Création de la fenêtre..."); + if (mainWindow) { - logger.warn("La fenetre existe deja, pas de nouvelle creation"); + logger.warn("La fenêtre existe déjà, pas de nouvelle création"); return; } + mainWindow = new BrowserWindow({ title: "Freedom Loader", width: 750, @@ -38,42 +45,92 @@ async function createWindow() { webPreferences: { nodeIntegration: false, contextIsolation: true, + preload: path.join(__dirname, "preload.js"), // Chargement du preload script sécurisé }, - // titleBarStyle: 'hidden', + // titleBarStyle: 'hidden', // Option possible pour une barre de titre personnalisée }); try { await mainWindow.loadURL("http://localhost:8080"); - logger.info("Fenetre chargee"); + logger.info("Fenêtre chargée"); } catch (err) { - logger.error("Erreur chargement fenetre:", err); + logger.error("Erreur chargement fenêtre:", err); } + // Événement déclenché à la fermeture de la fenêtre principale mainWindow.on("closed", () => { - logger.info("Fenetre principale fermee"); + logger.info("Fenêtre principale fermée"); mainWindow = null; }); } +/* + Définition du chemin par défaut pour les téléchargements. + Ici, on utilise le dossier 'Downloads' de l'utilisateur avec un sous-dossier spécifique à l'application. +*/ +const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader"); + +/* + Gestionnaire IPC qui permet au renderer de demander à l'utilisateur + de sélectionner un dossier via une boîte de dialogue native. + On retourne le chemin sélectionné ou null si annulation. +*/ +ipcMain.handle("select-download-folder", async () => { + logger.info("Demande de sélection d'un dossier reçue depuis le renderer"); + try { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"] + }); + if (!result.canceled && result.filePaths.length > 0) { + logger.info(`Dossier sélectionné : ${result.filePaths[0]}`); + return result.filePaths[0]; + } + return null; + } catch (err) { + logger.error(`Erreur lors de la sélection de dossier : ${err.message}`); + return null; + } +}); + +/* + Gestionnaire IPC pour exposer au renderer le chemin par défaut de téléchargement + afin qu'il puisse l'afficher ou l'utiliser comme valeur initiale. +*/ +ipcMain.handle("get-default-download-path", () => { + return defaultDownloadPath; +}); + +/* + Événement déclenché quand l'application est prête. + On démarre le serveur Express, puis on crée la fenêtre principale. + En cas d'erreur, on log et on quitte l'application proprement. +*/ app.whenReady().then(async () => { - logSessionStart(); - logger.info("App prete, demarrage du serveur Express..."); + logSessionStart(); + logger.info("App prête, démarrage du serveur Express..."); const expressServer = require("./server/server.js"); try { await expressServer.startServer(); - logger.info("Serveur Express demarre"); + logger.info("Serveur Express démarré"); await createWindow(); } catch (error) { - logger.error("Erreur serveur ou fenetre :", error); + logger.error("Erreur serveur ou fenêtre :", error); app.quit(); } }); +/* + Quitte l'application lorsque toutes les fenêtres sont fermées, + sauf sous macOS où il est habituel de garder l'application active. +*/ app.on("window-all-closed", () => { - logger.info("Toutes fenetres fermees, quitte l'app"); + logger.info("Toutes fenêtres fermées, quitte l'app"); if (process.platform !== "darwin") app.quit(); }); +/* + Avant de quitter l'application, on log la fin de session pour traçabilité. +*/ app.on("before-quit", () => { - logSessionEnd(); + logSessionEnd(); }); diff --git a/package-lock.json b/package-lock.json index fdf4cf8..23e9233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "freedom-loader", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freedom-loader", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "chalk": "^4.1.2", "debug": "^4.4.1", @@ -1153,9 +1153,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz", - "integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==", + "version": "22.16.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.0.tgz", + "integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 13da612..d35b284 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "freedom-loader", "productName": "Freedom Loader", - "version": "1.0.1", + "version": "1.0.2", "author": "MasterAcnolo", "description": "Freedom Loader, parce que nous le valons bien", "main": "main.js", diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..6a2f152 --- /dev/null +++ b/preload.js @@ -0,0 +1,31 @@ +/* + This file is part of Freedom Loader. + + Copyright (C) 2025 MasterAcnolo + + Freedom Loader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License. + + Freedom Loader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const { contextBridge, ipcRenderer } = require("electron"); + +/* + Expose dans le contexte global (window.electronAPI) deux méthodes sécurisées + pour que le renderer puisse interagir avec le main process via IPC. + + - getDefaultDownloadPath : récupère le chemin de téléchargement par défaut. + - selectDownloadFolder : ouvre la boîte de dialogue pour choisir un dossier. +*/ +contextBridge.exposeInMainWorld("electronAPI", { + getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"), + selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"), +}); diff --git a/public/assets/images/Vilbrequin.avif b/public/assets/images/Vilbrequin.avif new file mode 100644 index 0000000..e115f98 Binary files /dev/null and b/public/assets/images/Vilbrequin.avif differ diff --git a/public/assets/images/cyberpunk4.webp b/public/assets/images/cyberpunk4.webp new file mode 100644 index 0000000..bb0dfd5 Binary files /dev/null and b/public/assets/images/cyberpunk4.webp differ diff --git a/public/assets/images/goat.webp b/public/assets/images/goat.webp new file mode 100644 index 0000000..4411f79 Binary files /dev/null and b/public/assets/images/goat.webp differ diff --git a/public/assets/images/spicy.webp b/public/assets/images/spicy.webp new file mode 100644 index 0000000..da2b756 Binary files /dev/null and b/public/assets/images/spicy.webp differ diff --git a/public/index.html b/public/index.html index f25656b..d317159 100644 --- a/public/index.html +++ b/public/index.html @@ -18,12 +18,12 @@ - -
+ +
-

Freedom Loader

-

Because why not?

+

Freedom Loader

+

Because why not?

@@ -51,24 +51,40 @@
-
- - - - +
+
+ Sera enregistré dans + +
+ +
+
+
+ + +
+ + - + + + \ No newline at end of file diff --git a/public/script/chirac.js b/public/script/chirac.js new file mode 100644 index 0000000..fab95b6 --- /dev/null +++ b/public/script/chirac.js @@ -0,0 +1,33 @@ +/* + This file is part of Freedom Loader. + + Copyright (C) 2025 MasterAcnolo + + Freedom Loader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License. + + Freedom Loader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +function Chirac(){ + const title = document.getElementById("title"); + title.innerHTML = "Chirac Loader"; + + const subtitle = document.getElementById("subtitle"); + subtitle.innerHTML = "J'aime les pommes"; + subtitle.style.color= "black"; + + document.body.style.backgroundImage = "url('assets/images/goat2.webp')"; + document.body.style.backgroundSize = "cover"; + document.body.style.backgroundPosition = "center"; + document.body.style.backgroundAttachment = "fixed"; + + console.log("Je serai le président de tous les français"); +} \ No newline at end of file diff --git a/public/script/custompath.js b/public/script/custompath.js new file mode 100644 index 0000000..54eaaea --- /dev/null +++ b/public/script/custompath.js @@ -0,0 +1,71 @@ +/* + This file is part of Freedom Loader. + + Copyright (C) 2025 MasterAcnolo + + Freedom Loader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License. + + Freedom Loader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/* + Ce script attend que le DOM soit complètement chargé pour initialiser + l'affichage et la gestion du chemin de sauvegarde personnalisé. + + 1. Récupère le chemin de téléchargement par défaut depuis le main process via l'API exposée. + 2. Met à jour le texte affiché dans l'élément avec l'id "savePath". + 3. Crée un input caché nommé "savePath" dans le formulaire pour envoyer ce chemin lors du submit. + 4. Ajoute un écouteur sur le bouton "changePath" pour permettre à l'utilisateur + de choisir un dossier via une boîte de dialogue native. + 5. Met à jour l'affichage et la valeur cachée du chemin sélectionné. +*/ + +window.addEventListener("DOMContentLoaded", async () => { + const savePathElem = document.getElementById("savePath"); + + // 1️Essayer de charger depuis le localStorage + let savedPath = localStorage.getItem("customDownloadPath"); + + // 2️Sinon demander le chemin par défaut à l'API Electron + if (!savedPath) { + savedPath = await window.electronAPI.getDefaultDownloadPath(); + } + + // 3 Afficher le chemin + if (savePathElem) { + savePathElem.textContent = savedPath; + } + + // Créer l'input caché s'il n'existe pas déjà + let hidden = document.getElementById("savePathInput"); + if (!hidden) { + hidden = document.createElement("input"); + hidden.type = "hidden"; + hidden.name = "savePath"; + hidden.id = "savePathInput"; + document.getElementById("downloadForm").appendChild(hidden); + } + hidden.value = savedPath; + + // Gestion du bouton de modification + document.getElementById("changePath").addEventListener("click", async () => { + const selectedPath = await window.electronAPI.selectDownloadFolder(); + if (selectedPath) { + // Met à jour l'affichage + savePathElem.textContent = selectedPath; + hidden.value = selectedPath; + + // Et le stocke en localStorage pour la prochaine fois + localStorage.setItem("customDownloadPath", selectedPath); + } + }); +}); + diff --git a/public/script/customthemes.js b/public/script/customthemes.js new file mode 100644 index 0000000..f39c271 --- /dev/null +++ b/public/script/customthemes.js @@ -0,0 +1,105 @@ +/* + This file is part of Freedom Loader. + + Copyright (C) 2025 MasterAcnolo + + Freedom Loader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License. + + Freedom Loader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Liste des thèmes disponibles (clé = nom, valeur = label affiché) +const themes = { + default: "Default", + dark: "Sombre", + light: "Clair", + chirac: "Chirac", + fanatic: "Fanatic", + cyberpunk: "Cyberpunk", + spicy: "Spicy", + vilbrequin: "Vilbrequin" +}; + +const themeSubtitles = { + default: "Because why not?", + dark: "Darkness is my ally", + light: "Qui aime ce thème ?", + chirac: "J'aime les pommes", + fanatic: "Always Fnatic !", + cyberpunk: "Wake up, choom. We’ve got a city to burn.", + spicy: "The Spiciest One", + vilbrequin: "Rend l'argent" +}; + +const themeSelect = document.getElementById("themeSelect"); + +// Remplir le select avec les options +function populateThemeSelect() { + for (const [key, label] of Object.entries(themes)) { + const option = document.createElement("option"); + option.value = key; + option.textContent = label; + themeSelect.appendChild(option); + } +} + +// Appliquer le thème sur le body et changer le subtitle +function applyTheme(themeName) { + // Supprime les classes thème précédentes + document.body.classList.remove(...Object.keys(themes)); + // Ajoute la classe du thème actuel + document.body.classList.add(themeName); + + // Modifier le subtitle + const subtitle = document.getElementById("subtitle"); + if (subtitle && themeSubtitles[themeName]) { + subtitle.textContent = themeSubtitles[themeName]; + } + + // Optionnel : animation spéciale Chirac + if (themeName === "chirac") { + requestAnimationFrame(() => { + document.body.classList.add("theme-active"); + }); + } else { + document.body.classList.remove("theme-active"); + } +} + +// Sauvegarder le thème en localStorage +function saveTheme(themeName) { + localStorage.setItem("selectedTheme", themeName); +} + +// Charger le thème au démarrage +function loadTheme() { + const savedTheme = localStorage.getItem("selectedTheme"); + if (savedTheme && themes[savedTheme]) { + applyTheme(savedTheme); + themeSelect.value = savedTheme; + } else { + applyTheme("dark"); + themeSelect.value = "dark"; + } +} + +// Quand l’utilisateur change le select +themeSelect.addEventListener("change", (e) => { + const selected = e.target.value; + if (themes[selected]) { + applyTheme(selected); + saveTheme(selected); + } +}); + +// Initialisation +populateThemeSelect(); +loadTheme(); diff --git a/public/script/downloadstatus.js b/public/script/downloadstatus.js index 435efcf..9f30b03 100644 --- a/public/script/downloadstatus.js +++ b/public/script/downloadstatus.js @@ -19,53 +19,53 @@ // Récupération du formulaire de téléchargement const form = document.getElementById("downloadForm"); -// Div où on affiche l'état de progression (en cours, erreur, succès) +// Élément où on affichera l’état du téléchargement (en cours, erreur, succès) const statusDiv = document.getElementById("downloadStatus"); -// Récupération du bouton de validation pour pouvoir le désactiver pendant la requête +// Récupération du bouton de soumission pour pouvoir le désactiver pendant la requête const button = form.querySelector("button"); -// On intercepte la soumission du formulaire +// Écouteur d’événement pour intercepter la soumission du formulaire form.addEventListener("submit", async (e) => { - e.preventDefault(); // empêche le rechargement de la page par le navigateur + e.preventDefault(); // Empêche le rechargement automatique de la page - // On désactive le bouton pour éviter les clics multiples + // Désactive le bouton pour éviter les clics multiples pendant la requête button.disabled = true; - // Affiche un message d'attente à l'utilisateur + // Affiche un message d’attente pour informer l’utilisateur statusDiv.textContent = "Téléchargement en cours..."; - // Prépare les données du formulaire sous forme URL-encoded + // Prépare les données du formulaire au format URL-encoded const formData = new FormData(form); const params = new URLSearchParams(formData); try { - // Envoi de la requête POST vers /download avec les données du formulaire + // Envoie la requête POST vers /download avec les données du formulaire const res = await fetch("/download", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params, }); - // Si la réponse n'est pas un succès (code 4xx ou 5xx) + // Si la réponse n’est pas un succès (code HTTP 4xx ou 5xx) if (!res.ok) { statusDiv.textContent = "❌ Erreur pendant le téléchargement."; return; } - // On récupère le texte de la réponse du serveur + // Récupère le texte envoyé par le serveur (message de succès ou erreur) const text = await res.text(); - // Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé ») + // Affiche le message retourné par le serveur statusDiv.textContent = text; } catch { - // Gestion d'erreur en cas de problème réseau ou autre + // Gestion des erreurs réseau ou autres exceptions statusDiv.textContent = "❌ Une erreur s’est produite."; } finally { - // Réactive le bouton pour autoriser d'autres soumissions + // Réactive le bouton pour permettre d’autres soumissions button.disabled = false; - // Efface le message de status après 5 secondes pour garder l'UI propre + // Efface le message d’état après 5 secondes pour garder l’interface propre setTimeout(() => { statusDiv.textContent = ""; }, 5000); diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js index a60635d..4287f97 100644 --- a/public/script/fetchinfo.js +++ b/public/script/fetchinfo.js @@ -16,17 +16,25 @@ along with this program. If not, see . */ +// On attend que le DOM soit complètement chargé document.addEventListener("DOMContentLoaded", () => { + // Récupération des éléments du DOM pour interaction const urlInput = document.getElementById("UrlInput"); const infoDiv = document.getElementById("videoInfo"); + // Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles let lastFetchedUrl = ""; + + // Active les logs DEBUG pour le développement const DEBUG = true; + // Écoute les changements dans l'input URL urlInput.addEventListener("input", async () => { const url = urlInput.value.trim(); + if (DEBUG) console.log("[DEBUG] Input détecté :", url); + // Si l'URL est vide ou trop courte, on reset l'affichage if (!url || url.length < 5) { if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage."); infoDiv.innerHTML = ""; @@ -35,15 +43,19 @@ document.addEventListener("DOMContentLoaded", () => { return; } + // Si l'URL est identique à la dernière requêtée, on ne refait pas la requête if (url === lastFetchedUrl) { if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch."); return; } + // Mémorise l'URL actuelle comme dernière URL traitée lastFetchedUrl = url; try { if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url); + + // Envoi de la requête POST pour récupérer les infos de la vidéo const res = await fetch("/info", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, @@ -52,6 +64,7 @@ document.addEventListener("DOMContentLoaded", () => { if (DEBUG) console.log("[DEBUG] Réponse status:", res.status); + // Si la réponse HTTP est une erreur, on affiche un message if (!res.ok) { console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`); infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur)."; @@ -59,10 +72,11 @@ document.addEventListener("DOMContentLoaded", () => { return; } + // Parse la réponse JSON contenant les infos vidéo const data = await res.json(); if (DEBUG) console.log("[DEBUG] Données reçues:", data); - // Vérification minimaliste + // Vérification simple que les données attendues sont présentes if (!data || !data.title || !data.thumbnail) { console.warn("[WARNING] Données incomplètes", data); infoDiv.innerHTML = "❌ Données incomplètes reçues."; @@ -70,23 +84,28 @@ document.addEventListener("DOMContentLoaded", () => { return; } + // Formatage de la durée en minutes et secondes const minutes = Math.floor(data.duration / 60); const seconds = data.duration % 60; const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`; + // Formatage de la date d'upload au format JJ/MM/AAAA const date = data.upload_date || ""; const readableDate = date ? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}` : "Inconnue"; + // Conversion taille approximative en Mo const sizeMB = data.filesize_approx ? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo" : "Inconnue"; + // Gestion des catégories, affichage par défaut si absentes const categories = data.categories ? data.categories.join(", ") : "Non spécifiées"; + // Construction du HTML pour afficher les informations de la vidéo infoDiv.innerHTML = `

${data.title}

Thumbnail @@ -102,10 +121,13 @@ document.addEventListener("DOMContentLoaded", () => {
  • Catégories : ${categories}
  • `; + + // Affiche la div contenant les infos infoDiv.classList.add("visible"); if (DEBUG) console.log("[DEBUG] Info affichée avec succès."); } catch (e) { + // En cas d'erreur réseau ou JSON, on affiche un message d'erreur console.error("[CRITICAL] Erreur lors de la récupération:", e); infoDiv.innerHTML = "❌ Erreur réseau ou JSON."; infoDiv.classList.remove("visible"); diff --git a/public/styles/components/checkbox.css b/public/styles/components/checkbox.css index 2db0891..bfb8fc4 100644 --- a/public/styles/components/checkbox.css +++ b/public/styles/components/checkbox.css @@ -15,15 +15,15 @@ } .checkmark { - --clr: #3B82F6 ; + --clr: var(--checkbox-bg-checked); position: relative; top: 0; left: 0; height: 1.3em; width: 1.3em; - background-color: #ccc; - border-radius: 50%; - transition: 300ms; + background-color: var(--checkbox-bg-default); + border-radius: .5rem; + transition: background-color 300ms, border-radius 300ms; } .checkbox-mp3 input:checked ~ .checkmark { @@ -47,7 +47,7 @@ top: 0.25em; width: 0.25em; height: 0.5em; - border: solid #E0E0E2; + border: solid var(--checkbox-checkmark-border-color); border-width: 0 0.15em 0.15em 0; transform: rotate(45deg); } @@ -70,4 +70,4 @@ box-shadow: 0 0 0 13px var(--checkbox-pulse-color); rotate: 0; } -} \ No newline at end of file +} diff --git a/public/styles/components/editpathbutton.css b/public/styles/components/editpathbutton.css new file mode 100644 index 0000000..7668587 --- /dev/null +++ b/public/styles/components/editpathbutton.css @@ -0,0 +1,41 @@ +.download-path { + margin-top: 1em; + font-size: 1rem; + color: var(--default-text-color); +} + +.download-path #savePath:empty { + display: none; +} + +.download-path #savePath { + font-size: 1rem; + font-weight: 600; + background-color: rgba(0, 0, 0, 0.1); + color: var(--form-input-text-color); + padding: 0.3em 0.6em; + border-radius: 6px; + font-family: monospace; + border: 1px dashed var(--form-input-border-color); + word-break: break-all; + display: inline-block; + max-width: 80%; +} + + +.download-path button#changePath { + margin-left: 0.5em; + background-color: var(--form-button-bg-color); + color: var(--form-button-text-color); + border: none; + padding: 0.3em 0.8em; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +.download-path button#changePath:hover { + background-color: var(--form-button-bg-hover-color); + transform: scale(1.05); +} diff --git a/public/styles/components/themebutton.css b/public/styles/components/themebutton.css new file mode 100644 index 0000000..84693e6 --- /dev/null +++ b/public/styles/components/themebutton.css @@ -0,0 +1,38 @@ +.theme-switcher { + position: absolute; + top: 10px; + right: 10px; + background: #242424; + padding: 8px 12px; + border-radius: 8px; + color: #fff; + font-family: sans-serif; + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 8px; + z-index: 1000; +} + +.theme-switcher label { + cursor: pointer; + font-weight: 600; +} + +.theme-switcher select { + background: #fff; + border: 1px solid #242424; + border-radius: 6px; + padding: 4px 8px; + color: #000; + cursor: pointer; + font-weight: 600; + transition: background 0.3s ease, color 0.3s ease; +} + +.theme-switcher select:hover, +.theme-switcher select:focus { + background: #474747; + color: #fff; + outline: none; +} diff --git a/public/styles/layout/ctr-infos.css b/public/styles/layout/ctr-infos.css index ef1e29e..1eb8e74 100644 --- a/public/styles/layout/ctr-infos.css +++ b/public/styles/layout/ctr-infos.css @@ -4,7 +4,7 @@ min-height: 1.5em; font-weight: bold; text-align: center; - color: #007bff; + color: var(--download-status-color); } label { diff --git a/public/styles/layout/form.css b/public/styles/layout/form.css index faa7f41..0fac138 100644 --- a/public/styles/layout/form.css +++ b/public/styles/layout/form.css @@ -1,5 +1,5 @@ form { - background-color: #fff; + background-color: var(--form-bg-color); padding: 2em; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); @@ -13,8 +13,10 @@ form input[type="text"], form input[type="url"], form select { padding: 0.75em; - border: 1px solid #ccc; + border: 1px solid var(--form-input-border-color); border-radius: 8px; + background-color: var(--form-input-bg-color); + color: var(--form-input-text-color); } form input[type="checkbox"] { @@ -22,8 +24,8 @@ form input[type="checkbox"] { } form button { - background-color: #007bff; - color: #fff; + background-color: var(--form-button-bg-color); + color: var(--form-button-text-color); border: none; padding: 0.75em; border-radius: 8px; @@ -32,42 +34,43 @@ form button { } form button:hover { - background-color: #0056b3; + background-color: var(--form-button-bg-hover-color); } form#downloadForm { - display: flex; - gap: 0.5em; - max-width: 600px; - width: 100%; - justify-content: center; + display: flex; + gap: 0.5em; + max-width: 600px; + width: 100%; + justify-content: center; } .checkbox-inline { - display: flex; - align-items: center; - align-self: center; - gap: 0.5em; + display: flex; + align-items: center; + align-self: center; + gap: 0.5em; + color: var(--form-input-text-color); } #UrlInput { - padding: 0.75em 1em; - border-color: #ccc; - box-shadow: none; - border-radius: 8px; - font-size: 1rem; - transition: border-color 0.3s ease, box-shadow 0.3s ease; - background-color: #f9f9f9; - color: #333; + padding: 0.75em 1em; + border-color: var(--form-input-border-color); + box-shadow: none; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + background-color: var(--form-input-bg-color); + color: var(--form-input-text-color); } #UrlInput:focus { - border-color: #0056b3; - box-shadow: 0 0 5px rgba(0,123,255,0.4); - outline: none; - background-color: #fff; + border-color: var(--form-input-border-focus-color); + box-shadow: 0 0 5px rgba(0,123,255,0.4); + outline: none; + background-color: var(--form-input-bg-color); } #UrlInput::placeholder { - color: #aaa; + color: var(--form-input-placeholder-color); } diff --git a/public/styles/layout/videoinfo.css b/public/styles/layout/videoinfo.css index bc8a43c..4ed8b11 100644 --- a/public/styles/layout/videoinfo.css +++ b/public/styles/layout/videoinfo.css @@ -7,7 +7,7 @@ background-color: var(--infos-box-color); border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); - color: #222; + color: var(--video-info-text-color); font-size: 1rem; line-height: 1.5; opacity: 0; @@ -27,7 +27,7 @@ #videoInfo h3 { font-size: 1.8rem; margin-bottom: 0.6em; - color: #007bff; + color: var(--video-info-heading-color); font-weight: 700; word-break: break-word; } @@ -51,15 +51,15 @@ #videoInfo ul li { margin-bottom: 0.5em; font-weight: 500; - color: #444; + color: var(--video-info-list-color); } #videoInfo ul li strong { - color: #000; + color: var(--video-info-list-strong-color); } #videoInfo ul li a { - color: #007bff; + color: var(--video-info-link-color); text-decoration: none; } diff --git a/public/styles/styles.css b/public/styles/styles.css index 1f8f64c..16c0fa0 100644 --- a/public/styles/styles.css +++ b/public/styles/styles.css @@ -4,10 +4,22 @@ @import url("layout/header.css"); @import url("layout/videoinfo.css"); @import url("components/checkbox.css"); -@import url("variables.css"); +@import url("components/themebutton.css"); +/* @import url("variables.css"); */ +@import url("theme/themeimport.css"); +@import url("components/editpathbutton.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; +} + html,body{ margin: 0; padding: 0; @@ -34,6 +46,7 @@ body { display: flex; flex-direction: column; align-items: center; + } @@ -45,22 +58,11 @@ header p{ color: var(--mp3-text-color); } -/* #videoInfo { - margin-top: 100px; - padding: 1em; - max-width: 600px; - margin-left: auto; - margin-right: auto; - border-radius: 8px; - color: rgb(0, 0, 0); - box-shadow: 0 0 10px rgba(0,0,0,0.5); - transition: opacity 0.5s ease, max-height 0.5s ease; - opacity: 0; - max-height: 0; - overflow: hidden; -} - -#videoInfo.visible { - opacity: 1; - max-height: 1000px; -} */ +body, +.container, +input, +button, +select, +label { + transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease; +} \ No newline at end of file diff --git a/public/styles/theme/chirac-theme.css b/public/styles/theme/chirac-theme.css new file mode 100644 index 0000000..02b1cce --- /dev/null +++ b/public/styles/theme/chirac-theme.css @@ -0,0 +1,98 @@ +body.chirac { + /* Background image Chirac */ + background-image: url('../../assets/images/goat.webp'); + background-position: right; + background-repeat: no-repeat; + background-size: cover; + background-attachment: fixed; + + /* Couleurs drapeau français */ + --background-color: rgba(255, 255, 255, 0.85); /* blanc légèrement transparent pour lisibilité */ + --default-text-color: #002395; /* bleu foncé */ + --subtitle-color: #EF4135; /* rouge vif */ + --mp3-text-color: #002395; /* bleu */ + + /* 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; /* bleu */ + --form-button-text-color: #ffffff; + --form-button-bg-hover-color: #EF4135; /* rouge */ + + /* 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: #002395; + --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; + + /* animation d'apparition */ + opacity: 0; + transition: opacity 0.8s ease, background-color 0.8s ease; +} + +body.chirac.theme-active { + opacity: 1; +} + +/* + +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/cyberpunk-theme.css b/public/styles/theme/cyberpunk-theme.css new file mode 100644 index 0000000..434210f --- /dev/null +++ b/public/styles/theme/cyberpunk-theme.css @@ -0,0 +1,45 @@ +body.cyberpunk { + /* Couleurs générales */ + --background-color: #0d0d0d; /* noir profond */ + --default-text-color: #e0e0e0; /* texte gris clair */ + --subtitle-color: #ffea00; /* turquoise néon pour les sous-titres */ + --mp3-text-color: #ffea00; /* jaune vif pour la checkbox */ + + /* Formulaire */ + --form-bg-color: #141414; /* fond formulaire sombre */ + --form-input-bg-color: #1a1a1a; /* inputs sombres */ + --form-input-border-color: #333333; /* bordure sobre */ + --form-input-border-focus-color: #ffea00; /* focus jaune néon */ + --form-input-text-color: #e0e0e0; /* texte clair */ + --form-input-placeholder-color: #777777; /* placeholder plus doux */ + --form-button-bg-color: #ffea00; /* bouton jaune */ + --form-button-text-color: #0d0d0d; /* texte bouton noir */ + --form-button-bg-hover-color: #d4c000; /* hover jaune plus foncé */ + + /* Checkbox */ + --checkbox-bg-default: #333333; /* fond checkbox sobre */ + --checkbox-bg-checked: #ffea00; /* coché en jaune vif */ + --checkbox-checkmark-border-color: #0d0d0d; /* checkmark noir */ + --checkbox-pulse-color: rgba(255, 234, 0, 0.5); /* pulse néon jaune */ + + /* Download status */ + --download-status-color: #fffbd1; /* turquoise néon */ + + /* Video Info Box */ + --infos-box-color: #1a1a1a; /* fond infos */ + --video-info-text-color: #e0e0e0; /* texte clair */ + --video-info-heading-color: #ffea00; /* titres turquoise néon */ + --video-info-list-color: #cccccc; /* texte liste gris clair */ + --video-info-list-strong-color: #ffffff; /* texte important blanc */ + --video-info-link-color: #ffea00; /* liens jaune néon */ + --video-info-link-hover-color: #d4c000; /* liens hover plus foncé */ +} + +body.cyberpunk { + background-image: url('../../assets/images/cyberpunk4.webp'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; +} + diff --git a/public/styles/theme/dark-theme.css b/public/styles/theme/dark-theme.css new file mode 100644 index 0000000..534b815 --- /dev/null +++ b/public/styles/theme/dark-theme.css @@ -0,0 +1,36 @@ +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; +} diff --git a/public/styles/theme/default-theme.css b/public/styles/theme/default-theme.css new file mode 100644 index 0000000..a3fa903 --- /dev/null +++ b/public/styles/theme/default-theme.css @@ -0,0 +1,40 @@ +:root { + /* Couleurs générales */ + --background-color: #001224; /* body background bleu nuit */ + --default-text-color: #eee; /* texte clair principal */ + --subtitle-color: #007bff; /* sous-titres, header p */ + --mp3-text-color: #000000; /* couleur texte checkbox inline */ + + /* Formulaire */ + --form-bg-color: #ffffff; /* fond du formulaire */ + --form-input-bg-color: #f9f9f9; /* fond inputs */ + --form-input-border-color: #cccccc; /* bordure inputs */ + --form-input-border-focus-color: #0056b3; /* bordure focus */ + --form-input-text-color: #333333; /* texte inputs */ + --form-input-placeholder-color: #aaaaaa; /* placeholder */ + --form-button-bg-color: #007bff; /* bouton bg */ + --form-button-text-color: #ffffff; /* texte bouton */ + --form-button-bg-hover-color: #0056b3; /* bouton hover */ + + /* Checkbox */ + --checkbox-bg-default: #cccccc; + --checkbox-bg-checked: #3B82F6; + --checkbox-checkmark-border-color: #E0E0E2; + --checkbox-pulse-color: rgba(59, 130, 246, 0.5); + + /* Download status */ + --download-status-color: #007bff; + + /* Video Info Box */ + --infos-box-color: #f0f0f0; + --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; +} + +body.default .download-path #savePath { + color: #ffffff; +} \ No newline at end of file diff --git a/public/styles/theme/fanatic-theme.css b/public/styles/theme/fanatic-theme.css new file mode 100644 index 0000000..8553866 --- /dev/null +++ b/public/styles/theme/fanatic-theme.css @@ -0,0 +1,36 @@ +body.fanatic{ + /* Couleurs générales */ + --background-color: #121212; /* fond bleu nuit/noir */ + --default-text-color: #eee; /* texte clair principal */ + --subtitle-color: #FF5900; /* sous-titres, accent orange vif */ + --mp3-text-color: #ffffff; /* texte checkbox inline */ + + /* Formulaire */ + --form-bg-color: #1e1e1e; /* fond formulaire sombre */ + --form-input-bg-color: #2b2b2b; /* fond inputs sombres */ + --form-input-border-color: #333333; /* bordure inputs */ + --form-input-border-focus-color: #FF5900; /* bordure focus orange */ + --form-input-text-color: #eeeeee; /* texte inputs clair */ + --form-input-placeholder-color: #666666; /* placeholder gris foncé */ + --form-button-bg-color: #FF5900; /* bouton orange */ + --form-button-text-color: #121212; /* texte bouton sombre */ + --form-button-bg-hover-color: #e65500; /* bouton hover orange foncé */ + + /* Checkbox */ + --checkbox-bg-default: #555555; /* gris moyen */ + --checkbox-bg-checked: #ff6600; /* orange vif */ + --checkbox-checkmark-border-color: #eee; /* blanc cassé */ + --checkbox-pulse-color: rgba(255, 102, 0, 0.5); /* pulse orange translucide */ + + /* Download status */ + --download-status-color: #FF5900; /* orange */ + + /* Video Info Box */ + --infos-box-color: #222222; /* fond infos sombre */ + --video-info-text-color: #eee; /* texte clair */ + --video-info-heading-color: #FF5900; /* titres orange */ + --video-info-list-color: #ccc; /* texte liste gris clair */ + --video-info-list-strong-color: #fff; /* texte important blanc */ + --video-info-link-color: #FF5900; /* liens orange */ + --video-info-link-hover-color: #e65500; /* liens hover orange foncé */ +} diff --git a/public/styles/theme/light-theme.css b/public/styles/theme/light-theme.css new file mode 100644 index 0000000..cd562b7 --- /dev/null +++ b/public/styles/theme/light-theme.css @@ -0,0 +1,36 @@ +body.light { + /* Couleurs générales */ + --background-color: #dadada; /* fond clair */ + --default-text-color: #222222; /* texte sombre */ + --subtitle-color: #007bff; /* bleu accent */ + --mp3-text-color: #000000; /* texte checkbox */ + + /* Formulaire */ + --form-bg-color: #ffffff; /* fond formulaire blanc */ + --form-input-bg-color: #fafafa; /* fond inputs */ + --form-input-border-color: #cccccc; /* bordure inputs */ + --form-input-border-focus-color: #007bff; /* focus bleu */ + --form-input-text-color: #333333; /* texte inputs */ + --form-input-placeholder-color: #888888; /* placeholder */ + --form-button-bg-color: #007bff; /* bouton bleu */ + --form-button-text-color: #ffffff; /* texte bouton */ + --form-button-bg-hover-color: #0056b3; /* hover bleu foncé */ + + /* 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; +} diff --git a/public/styles/theme/spicy-theme.css b/public/styles/theme/spicy-theme.css new file mode 100644 index 0000000..b4f2c93 --- /dev/null +++ b/public/styles/theme/spicy-theme.css @@ -0,0 +1,44 @@ +body.spicy { + /* Couleurs générales */ + --background-color: #121212; /* fond noir profond */ + --default-text-color: #f5f5f5; /* texte clair */ + --subtitle-color: #e29191; /* 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é */ +} + +/* Optionnel: background piments */ +body.spicy { + background-image: url('../../assets/images/spicy.webp'); + background-size: cover; + background-position: center; + background-attachment: fixed; +} diff --git a/public/styles/theme/themeimport.css b/public/styles/theme/themeimport.css new file mode 100644 index 0000000..82d1565 --- /dev/null +++ b/public/styles/theme/themeimport.css @@ -0,0 +1,8 @@ +@import url("chirac-theme.css"); +@import url("dark-theme.css"); +@import url("default-theme.css"); +@import url("fanatic-theme.css"); +@import url("light-theme.css"); +@import url("cyberpunk-theme.css"); +@import url("spicy-theme.css"); +@import url("vilbrequin-theme.css"); \ No newline at end of file diff --git a/public/styles/theme/vilbrequin-theme.css b/public/styles/theme/vilbrequin-theme.css new file mode 100644 index 0000000..9a89dea --- /dev/null +++ b/public/styles/theme/vilbrequin-theme.css @@ -0,0 +1,48 @@ +body.vilbrequin { + /* Couleurs générales */ + --background-color: #0a1f3d; /* bleu Vilbrequin profond */ + --default-text-color: #535353; /* texte clair */ + --subtitle-color: #001696; /* bleu clair accent (turbo vibes) */ + --mp3-text-color: #ffffff; /* texte checkbox inline */ + + /* Formulaire */ + --form-bg-color: #12284f; /* fond formulaire bleu foncé */ + --form-input-bg-color: #1c3760; /* fond inputs bleu + clair */ + --form-input-border-color: #2a4b80; /* bordure bleue */ + --form-input-border-focus-color: #00bfff; /* focus bleu clair */ + --form-input-text-color: #f0f0f0; /* texte inputs clair */ + --form-input-placeholder-color: #b0c4de; /* placeholder gris bleuté */ + --form-button-bg-color: #ffd700; /* bouton jaune turbo */ + --form-button-text-color: #0a1f3d; /* texte bouton sombre */ + --form-button-bg-hover-color: #e6c200; /* hover jaune foncé */ + + /* Checkbox */ + --checkbox-bg-default: #2a4b80; /* bleu moyen */ + --checkbox-bg-checked: #00bfff; /* bleu clair accent */ + --checkbox-checkmark-border-color: #f0f0f0; /* blanc cassé */ + --checkbox-pulse-color: rgba(0,191,255,0.5); /* pulse bleu clair translucide */ + + /* Download status */ + --download-status-color: #ffd700; /* jaune turbo */ + + /* Video Info Box */ + --infos-box-color: #12284f; /* fond infos bleu foncé */ + --video-info-text-color: #f0f0f0; /* texte clair */ + --video-info-heading-color: #00bfff; /* titres bleu clair */ + --video-info-list-color: #dcdcdc; /* texte liste gris clair */ + --video-info-list-strong-color: #ffffff; /* texte important blanc */ + --video-info-link-color: #00bfff; /* liens bleu clair */ + --video-info-link-hover-color: #009acd; /* liens hover bleu plus foncé */ +} + +/* Optionnel: motif turbo ou jantes */ +body.vilbrequin { + background-image: url('../../assets/images/Vilbrequin.avif'); /* adapte ton image */ + background-size: cover; + background-position: center; + background-attachment: fixed; +} + +body.vilbrequin .download-path{ + color: white; +} \ No newline at end of file diff --git a/public/styles/variables.css b/public/styles/variables.css index fb2b848..7061e41 100644 --- a/public/styles/variables.css +++ b/public/styles/variables.css @@ -1,11 +1,36 @@ -:root{ - --background-color : #0F172A; - --infos-box-color: #f1f1f1 ; - --title-color: ; - --subtitle-color : #a3aab4; - --mp3-text-color: #0F172A; - --default-text-color: #F8FAFC; +:root { + /* Couleurs générales */ + --background-color: #001224; /* body background bleu nuit */ + --default-text-color: #eee; /* texte clair principal */ + --subtitle-color: #007bff; /* sous-titres, header p */ + --mp3-text-color: #000000; /* couleur texte checkbox inline */ - --checkbox-pulse-color:#9bbcf1; + /* Formulaire */ + --form-bg-color: #ffffff; /* fond du formulaire */ + --form-input-bg-color: #f9f9f9; /* fond inputs */ + --form-input-border-color: #cccccc; /* bordure inputs */ + --form-input-border-focus-color: #0056b3; /* bordure focus */ + --form-input-text-color: #333333; /* texte inputs */ + --form-input-placeholder-color: #aaaaaa; /* placeholder */ + --form-button-bg-color: #007bff; /* bouton bg */ + --form-button-text-color: #ffffff; /* texte bouton */ + --form-button-bg-hover-color: #0056b3; /* bouton hover */ -} \ No newline at end of file + /* Checkbox */ + --checkbox-bg-default: #cccccc; + --checkbox-bg-checked: #3B82F6; + --checkbox-checkmark-border-color: #E0E0E2; + --checkbox-pulse-color: rgba(59, 130, 246, 0.5); + + /* Download status */ + --download-status-color: #007bff; + + /* Video Info Box */ + --infos-box-color: #f0f0f0; + --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; +} diff --git a/server/logger.js b/server/logger.js index d8b5cb8..068a1d6 100644 --- a/server/logger.js +++ b/server/logger.js @@ -16,53 +16,58 @@ along with this program. If not, see . */ -const { createLogger, format, transports } = require("winston"); -const DailyRotateFile = require("winston-daily-rotate-file"); +const { createLogger, format, transports } = require("winston"); +const DailyRotateFile = require("winston-daily-rotate-file"); +const fs = require("fs"); // Pour vérifier/créer le dossier de logs +const path = require("path"); // Pour gérer proprement les chemins -const fs = require("fs"); // pour vérifier/créer le dossier de logs -const path = require("path"); // pour gérer proprement les chemins - -// on définit le dossier de logs +// Définition du dossier où seront stockés les logs const logDir = path.join(__dirname, "../logs"); -// on le crée si inexistant + +// Création du dossier logs s’il n’existe pas encore if (!fs.existsSync(logDir)) fs.mkdirSync(logDir); -// fonction pour générer une ligne de début de session +// Fonction utilitaire pour générer une ligne indiquant le début d’une session de logs function getSessionStartLine() { const now = new Date().toISOString(); return `--- Démarrage de la session : ${now} ---`; } -// fonction pour générer une ligne de fin de session +// Fonction utilitaire pour générer une ligne indiquant la fin d’une session de logs function getSessionEndLine() { const now = new Date().toISOString(); return `--- Fin de la session : ${now} ---`; } -// configuration principale de Winston +// Configuration principale de Winston +// - Niveau minimum d’écriture : info (enregistre info, warn, error, etc.) +// - Formatage des logs avec timestamp lisible et format personnalisé +// - Transports : écriture dans fichier journalier + console avec couleurs const logger = createLogger({ - level: "info", // niveau de log minimum (info, warn, error, etc.) + level: "info", format: format.combine( - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf(({ timestamp, level, message }) => - `${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log + `${timestamp} | ${level.toUpperCase()} | ${message}` ) ), transports: [ + // Rotation quotidienne des fichiers de logs new DailyRotateFile({ - dirname: logDir, // dossier de logs - filename: "LOGS-%DATE%.log", // nom de fichier journalier - datePattern: "YYYY-MM-DD", // pattern de rotation - zippedArchive: false, // true = compression des archives - maxFiles: "14d", // garde 14 jours d’historique + dirname: logDir, // dossier cible pour les logs + filename: "LOGS-%DATE%.log", // nom du fichier par date + datePattern: "YYYY-MM-DD", // pattern de date dans le nom + zippedArchive: false, // ne pas compresser les archives + maxFiles: "7d", // conserve 14 jours d’historique format: format.combine( format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}` ) ), - options: { flags: "a" }, // mode append + options: { flags: "a" }, // mode ajout à la fin (append) }), + // Affichage des logs en console avec coloration et format simple new transports.Console({ format: format.combine( format.colorize(), @@ -75,17 +80,17 @@ const logger = createLogger({ ], }); -// fonction d'utilitaire pour marquer le début de la session +// Helper pour marquer clairement le début de session dans les logs function logSessionStart() { logger.info(getSessionStartLine()); } -// pour la fin de la session +// Helper pour marquer la fin de session dans les logs function logSessionEnd() { logger.info(getSessionEndLine()); } -// exporte le logger et les helpers pour les autres modules +// Export du logger principal et des helpers pour usage dans d’autres modules module.exports = { logger, logSessionStart, diff --git a/server/routes/download.js b/server/routes/download.js index bed0fe9..30ad166 100644 --- a/server/routes/download.js +++ b/server/routes/download.js @@ -11,155 +11,150 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . */ -const express = require("express"); // importe le framework Express -const router = express.Router(); // crée un routeur Express pour modulariser les routes -const { execFile } = require("child_process"); // permet d’exécuter un binaire externe sans passer par un shell -const path = require("path"); // module Node.js pour gérer les chemins de fichiers -const fs = require("fs"); // module Node.js pour manipuler le système de fichiers +const express = require("express"); +const router = express.Router(); +const { execFile } = require("child_process"); +const path = require("path"); +const fs = require("fs"); -const logger = require("../logger").logger; // récupère l'instance de logger Winston personnalisée +const logger = require("../logger").logger; -// chemin vers l’exécutable yt-dlp packagé avec l’appli +// Path vers le fichier exécutable yt-dlp (outil tiers pour le téléchargement) const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe'); -// route POST sur la racine (ex: POST /download) router.post("/", (req, res) => { try { - // récupère les options envoyées depuis le frontend (via formulaire ou requête AJAX) + // Récupération des options envoyées par le frontend via le formulaire + // On vérifie la présence de l'URL, le choix audioOnly, et la qualité demandée const options = { - url: req.body.url, // URL à télécharger - audioOnly: req.body.audioOnly === "1", // conversion audio uniquement (mp3) - quality: req.body.quality || "best", // qualité vidéo à télécharger - subtitles: req.body.subs === "1", // récupérer les sous-titres anglais + url: req.body.url, + audioOnly: req.body.audioOnly === "1", + quality: req.body.quality || "best", }; + // Validation simple : si l'URL est manquante, on rejette la requête if (!options.url) { - // si l’URL est absente, on log et on renvoie 400 logger.warn("Requête POST /download sans URL"); return res.status(400).send("❌ URL manquante !"); } - // on récupère le dossier de sortie depuis app.locals (défini ailleurs dans le serveur) - const outputFolder = req.app.locals.outputFolder; + // Récupération du chemin de sauvegarde personnalisé envoyé depuis Electron, + // ou on utilise un chemin par défaut stocké dans app.locals + let requestedOutputFolder = req.body.savePath || req.app.locals.outputFolder; - // s'il n'existe pas, on le crée automatiquement - if (!fs.existsSync(outputFolder)) { - fs.mkdirSync(outputFolder, { recursive: true }); - logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements."); + // On normalise le chemin pour éviter des erreurs liées aux séparateurs ou chemins relatifs + requestedOutputFolder = path.normalize(requestedOutputFolder); + + /* + Sécurité basique : On refuse certains chemins sensibles ou suspects + (ex: System32, /etc, Windows) pour éviter que l'application n'écrive dans des dossiers système. + On vérifie aussi la longueur minimale du chemin pour éviter des valeurs vides ou invalides. + */ + if ( + !requestedOutputFolder || + requestedOutputFolder.length < 3 || + requestedOutputFolder.includes("System32") || + requestedOutputFolder.includes("/etc") || + requestedOutputFolder.includes("\\Windows") + ) { + logger.warn(`Chemin potentiellement dangereux refusé : ${requestedOutputFolder}`); + return res.status(400).send("❌ Chemin de sauvegarde non autorisé."); } - // template du nom de fichier de sortie (ex: MonTitre.mp4 ou MonTitre.mp3) - const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s"); + // Vérifie que le dossier de sortie existe, sinon le crée récursivement + if (!fs.existsSync(requestedOutputFolder)) { + fs.mkdirSync(requestedOutputFolder, { recursive: true }); + logger.info(`Dossier de sortie créé : ${requestedOutputFolder}`); + } - // tableau d'arguments pour yt-dlp - const args = []; + // Construction du template de sortie pour yt-dlp (nom de fichier basé sur le titre) + const outputTemplate = path.join(requestedOutputFolder, "%(title)s.%(ext)s"); + // Construction du tableau d'arguments à passer à yt-dlp + // --no-continue pour forcer un téléchargement propre + // --restrict-filenames pour éviter les caractères problématiques dans les noms + const args = ["--no-continue", "--restrict-filenames"]; + + // Si l'option audioOnly est activée, on ajoute les flags pour extraction audio en mp3 if (options.audioOnly) { - // si audioOnly, on force extraction audio en mp3 args.push("--extract-audio", "--audio-format", "mp3"); } - if (options.subtitles) { - // si l’utilisateur veut des sous-titres - args.push("--write-subs", "--sub-lang", "en"); - } + // Correspondance entre la qualité choisie et le format à demander à yt-dlp + const qualityMap = { + best: "bestvideo+bestaudio/best", + medium: "bestvideo[height<=720]+bestaudio/best[height<=720]", + worst: "worstvideo+worstaudio/worst", + 1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]", + 720: "bestvideo[height<=720]+bestaudio/best[height<=720]", + 480: "bestvideo[height<=480]+bestaudio/best[height<=480]", + }; - // // qualité souhaitée - // args.push("-f", options.quality); + // Sélection du format basé sur la qualité, par défaut 'best' si option invalide + const format = qualityMap[options.quality] || "best"; + args.push("-f", format); - // let format; - // if (options.quality === "best") { - // format = "bestvideo+bestaudio/best"; - // } else if (options.quality === "worst") { - // format = "worstvideo+worstaudio/worst"; - // } else { - // format = "best"; // fallback - // } - // args.push("-f", format); - - args.push("--no-continue"); - - let format; - - switch (options.quality) { - case "best": - format = "bestvideo+bestaudio/best"; - break; - case "medium": - // qualité moyenne, ex: 720p vidéo + meilleur audio - format = "bestvideo[height<=720]+bestaudio/best[height<=720]"; - break; - case "worst": - format = "worstvideo+worstaudio/worst"; - break; - case "1080": - format = "bestvideo[height<=1080]+bestaudio/best[height<=1080]"; - break; - case "720": - format = "bestvideo[height<=720]+bestaudio/best[height<=720]"; - break; - case "480": - format = "bestvideo[height<=480]+bestaudio/best[height<=480]"; - break; - default: - format = "best"; - } - args.push("-f", format); - - // modèle de sortie + // Ajout du template de sortie et de l'URL à télécharger args.push("-o", outputTemplate); - // enfin, l'URL à télécharger args.push(options.url); - // log de la commande complète pour debug + // Log de la commande complète pour traçabilité et débogage + logger.info(`Téléchargement demandé : url=${options.url}, audioOnly=${options.audioOnly}, quality=${options.quality}`); logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`); - // exécution du binaire yt-dlp + // Lancement du processus yt-dlp avec les arguments définis const child = execFile(ytDlpPath, args); - // gestion des logs stdout de yt-dlp + /* + Gestion des sorties standards (stdout) du processus, + on log chaque ligne pour suivre la progression ou informations diverses + */ child.stdout.on("data", (data) => { data.toString().split("\n").forEach(line => { - if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`); + if (line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`); }); }); - // gestion des logs stderr de yt-dlp (les erreurs) + /* + Gestion des erreurs (stderr) du processus, + on log chaque ligne d'erreur pour diagnostic + */ child.stderr.on("data", (data) => { data.toString().split("\n").forEach(line => { - if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`); + if (line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`); }); }); - // si une erreur système survient (par ex : yt-dlp.exe introuvable) + /* + En cas d'erreur lors du lancement du processus yt-dlp, + on log l'erreur et on répond au client avec un statut 500 + */ child.on("error", (err) => { logger.error(`Erreur lancement yt-dlp : ${err.message}`); res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`); }); - // callback quand le processus se termine + /* + Quand le processus se termine, on vérifie le code de sortie. + Code 0 = succès, on informe le client que le téléchargement est terminé. + Sinon, on envoie une erreur au client avec le code d'échec. + */ child.on("close", (code) => { logger.info(`yt-dlp terminé avec code de sortie : ${code}`); - if(code === 0) { - // succès + if (code === 0) { res.send("✅ Téléchargement terminé !"); } else { - // yt-dlp a échoué res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`); } }); } catch (err) { - // en cas d'erreur JavaScript + // Capture toute autre erreur serveur non prévue, log et réponse 500 logger.error(`Erreur serveur dans /download : ${err.message}`); res.status(500).send(`Erreur serveur : ${err.message}`); } }); -// on exporte ce routeur pour qu'il soit utilisé par l'app principale -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/server/routes/info.js b/server/routes/info.js index 372e823..a1aae33 100644 --- a/server/routes/info.js +++ b/server/routes/info.js @@ -16,40 +16,66 @@ along with this program. If not, see . */ -const express = require("express"); // importe Express -const router = express.Router(); // crée un routeur Express indépendant -const { execFile } = require("child_process"); // permet d’exécuter un programme externe -const path = require("path"); // module pour gérer les chemins de fichiers +const express = require("express"); +const router = express.Router(); +const { execFile } = require("child_process"); +const path = require("path"); +const fs = require("fs"); +const { logger } = require("../logger"); // On récupère ton logger Winston -// chemin vers le binaire yt-dlp embarqué +// Path absolu vers l'exécutable yt-dlp const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe"); -// route POST sur la racine de ce routeur +// Vérification que yt-dlp.exe existe bien au lancement du module +if (!fs.existsSync(ytDlpPath)) { + logger.error(`❌ yt-dlp.exe introuvable à ${ytDlpPath}`); + throw new Error(`yt-dlp.exe introuvable à ${ytDlpPath}`); +} + +// Route POST pour récupérer les métadonnées d'une vidéo via yt-dlp router.post("/", (req, res) => { - const url = req.body.url; // récupère l'URL depuis la requête POST - if (!url) return res.status(400).send("❌ URL manquante"); // validation simple + const url = req.body.url; - // exécute yt-dlp avec --dump-json pour récupérer les métadonnées de la vidéo - execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => { - if (error) { - // gestion d’erreur : par exemple yt-dlp plante ou l’URL est invalide - console.error("Erreur lors de l'exécution de yt-dlp :", error); - console.error("stderr :", stderr); - return res.status(500).send("❌ Impossible de récupérer les infos."); - } + // Validation basique : on refuse la requête sans URL + if (!url) { + logger.warn("Requête metadata sans URL"); + return res.status(400).send("❌ URL manquante"); + } - try { - // on parse le JSON envoyé par yt-dlp - const info = JSON.parse(stdout); - // on renvoie l’objet JSON directement au frontend - res.json(info); - } catch (e) { - // le JSON est mal formé - console.error("Erreur lors du parsing JSON :", e); - res.status(500).send("❌ JSON illisible."); + logger.info(`Requête metadata reçue pour ${url}`); + + // Exécution de yt-dlp avec l'option --dump-json pour récupérer les infos vidéo + execFile( + ytDlpPath, + ["--dump-json", url], + { timeout: 10_000 }, // Timeout fixé à 10 secondes pour éviter blocage + (error, stdout, stderr) => { + if (error) { + // En cas d'erreur, on log l'erreur et le stderr, puis on renvoie un code 500 + logger.error(`Erreur exécution yt-dlp: ${error.message}`); + logger.debug(`stderr: ${stderr}`); + return res.status(500).send("❌ Impossible de récupérer les infos."); + } + + try { + // yt-dlp peut retourner plusieurs JSON séparés par des sauts de ligne + // On split et parse chacun proprement + const infos = stdout + .trim() + .split("\n") + .map(line => JSON.parse(line)); + + logger.info(`Infos récupérées pour ${url} (${infos.length} élément(s))`); + + // Si on a un seul élément, on renvoie directement l'objet JSON, sinon un tableau + res.json(infos.length === 1 ? infos[0] : infos); + } catch (e) { + // Si parsing JSON échoue, on log et renvoie une erreur 500 + logger.error(`Erreur parsing JSON: ${e.message}`); + return res.status(500).send("❌ JSON illisible."); + } } - }); + ); }); -// on exporte le routeur pour être utilisé dans app.js ou ailleurs module.exports = router; diff --git a/server/server.js b/server/server.js index ba05351..54ba18f 100644 --- a/server/server.js +++ b/server/server.js @@ -16,88 +16,88 @@ along with this program. If not, see . */ -const express = require("express"); // framework web -const { exec } = require("child_process"); // si besoin d’exécuter des commandes (pas utilisé ici directement) -const fs = require("fs"); // accès au système de fichiers -const path = require("path"); // gestion de chemins +const express = require("express"); // Framework web pour créer l’API et servir les fichiers +const { exec } = require("child_process"); // Permet d’exécuter des commandes systèmes (pas utilisé directement ici) +const fs = require("fs"); // Module système pour gérer fichiers et dossiers +const path = require("path"); // Gestion propre des chemins de fichiers et dossiers const debug = require("debug")("freedom-loader:server"); -// petit debugger coloré en console +// Module de debug coloré en console, pratique pour dev const { logger, logSessionStart, logSessionEnd } = require("./logger"); -// récupère notre logger Winston + helpers de session +// Import du logger Winston et helpers pour marquer début/fin de session -const app = express(); // instancie l'application Express +const app = express(); // Création de l’instance Express, notre serveur web -// logSessionStart(); // désactivé, mais pourrait logguer le début de session - -// on définit le dossier de téléchargement dans le dossier Téléchargements de l’utilisateur +// Définition du dossier par défaut où enregistrer les téléchargements +// On prend le dossier Téléchargements de l’utilisateur Windows (USERPROFILE) const downloadsPath = path.join(process.env.USERPROFILE, "Downloads"); const outputFolder = path.join(downloadsPath, "Freedom Loader Output"); -// création du dossier s’il n’existe pas +// Création du dossier de sortie s’il n’existe pas déjà if (!fs.existsSync(outputFolder)) { try { - fs.mkdirSync(outputFolder, { recursive: true }); + fs.mkdirSync(outputFolder, { recursive: true }); // création récursive au cas où logger.info("Dossier Freedom Loader Output cree dans Telechargements."); } catch (err) { logger.error("Impossible de creer le dossier :", err); - process.exit(1); // arrêt du programme en cas d’échec + process.exit(1); // Arrêt du programme si dossier non créé (critique) } } else { logger.info("Dossier Freedom Loader Output deja existant."); } -// on rend ce dossier dispo via app.locals +// On rend ce dossier accessible globalement via app.locals pour l’utiliser dans les routes app.locals.outputFolder = outputFolder; -// configuration pour parser les requêtes POST en urlencoded +// Middleware pour parser le corps des requêtes POST en application/x-www-form-urlencoded app.use(express.urlencoded({ extended: true })); -// sert les fichiers statiques (frontend) +// Définition du dossier contenant les fichiers statiques (frontend) +// Permet d’accéder au HTML, CSS, JS côté client const staticPath = path.join(__dirname, "../public"); debug("Serveur statique sur", staticPath); app.use(express.static(staticPath)); -// enregistre les routes API +// Import et enregistrement des routes API +// Ces routes gèrent les requêtes pour les infos vidéo et téléchargement const infoRoute = require("./routes/info"); const downloadRoute = require("./routes/download"); debug("Routes /download et /info installees"); app.use("/download", downloadRoute); app.use("/info", infoRoute); -// route GET / de base +// Route GET / qui sert la page principale index.html app.get("/", (req, res) => { debug("Requete GET / servie"); res.sendFile(path.join(__dirname, "../public/index.html")); }); -// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs) +// Fonction pour démarrer le serveur Express +// Retourne une Promise pour pouvoir await le démarrage dans d’autres modules function startServer() { return new Promise((resolve, reject) => { logger.info("Demarrage du serveur Express..."); const serverInstance = app.listen(8080, () => { logger.info("Serveur Express pret sur http://localhost:8080"); - resolve(serverInstance); // succès + resolve(serverInstance); // Serveur prêt, on résout la promesse }); + // Gestion des erreurs serveur lors du démarrage serverInstance.on("error", (err) => { logger.error("Erreur serveur Express :", err); - reject(err); + reject(err); // Rejet de la promesse en cas d’erreur critique }); }); } -// gestion de la fermeture propre du process -// process.on("exit", () => { -// logSessionEnd(); -// }); -process.on("SIGINT", () => { // Ctrl+C +// Gestion propre de la fermeture du process pour logger la fin de session +process.on("SIGINT", () => { // Capture Ctrl+C (interruption) logSessionEnd(); process.exit(); }); -process.on("SIGTERM", () => { // kill +process.on("SIGTERM", () => { // Capture kill ou arrêt du process logSessionEnd(); process.exit(); }); -// on exporte la fonction startServer pour que d’autres modules puissent la lancer +// Export de la fonction startServer pour permettre son appel depuis d’autres fichiers module.exports = { startServer };