mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
refactor: add full documentation and fix theme loading robustness
- Add JSDoc comments across frontend and backend modules - Improve code readability and maintainability with consistent documentation style - Document Electron IPC handlers, UI utilities, and theme system - Add missing function/class documentation in theme loader and app services - Minor structural improvements and cleanup across Electron main process modules - Minor fixes on variable names
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
async function versionLabel(){
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
/**
|
||||
* Retrieves the application version from the Electron main process
|
||||
* and displays it in the UI version badge (bottom-right corner).
|
||||
*
|
||||
* This is mainly used for debugging and build identification.
|
||||
*/
|
||||
async function versionLabel() {
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
|
||||
// Write in front the app version for debugging (bottom right)
|
||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
||||
};
|
||||
/**
|
||||
* UI element displaying the current application version.
|
||||
* Updated at runtime after IPC call resolves.
|
||||
*/
|
||||
const versionBadge = document.getElementById("version-badge");
|
||||
|
||||
if (!versionBadge) return;
|
||||
|
||||
versionBadge.textContent = `v${appVersion}`;
|
||||
}
|
||||
|
||||
versionLabel();
|
||||
@@ -1,35 +1,42 @@
|
||||
// Clipboard paste functionality for URL input
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pasteBtn = document.getElementById('pasteBtn');
|
||||
const urlInput = document.getElementById('UrlInput');
|
||||
/**
|
||||
* Initializes clipboard paste functionality for the URL input field.
|
||||
*
|
||||
* When the user clicks the paste button, the system attempts to read
|
||||
* the clipboard content and inject it into the URL input field.
|
||||
* It also triggers an input event to notify any bound listeners.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const pasteBtn = document.getElementById("pasteBtn");
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
|
||||
if (pasteBtn && urlInput) {
|
||||
pasteBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
// Read text from clipboard
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
// Paste into the URL input
|
||||
urlInput.value = text;
|
||||
|
||||
// Trigger input event to ensure any listeners are notified
|
||||
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
// Optional: Focus the input
|
||||
urlInput.focus();
|
||||
|
||||
// Visual feedback
|
||||
pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)';
|
||||
setTimeout(() => {
|
||||
pasteBtn.style.backgroundColor = '';
|
||||
}, 200);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard:', err);
|
||||
|
||||
// Fallback: prompt user if clipboard API fails
|
||||
alert('Unable to access clipboard. Please paste manually using Ctrl+V.');
|
||||
}
|
||||
});
|
||||
if (!pasteBtn || !urlInput) return;
|
||||
|
||||
/**
|
||||
* Handles clipboard paste action triggered by the paste button.
|
||||
* Reads clipboard text and inserts it into the URL input field.
|
||||
*/
|
||||
pasteBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
urlInput.value = text;
|
||||
|
||||
// Notify frameworks / listeners bound to input changes
|
||||
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
urlInput.focus();
|
||||
|
||||
// Temporary UI feedback on paste action
|
||||
pasteBtn.style.backgroundColor = "var(--form-button-bg-hover-color)";
|
||||
setTimeout(() => {
|
||||
pasteBtn.style.backgroundColor = "";
|
||||
}, 200);
|
||||
} catch (err) {
|
||||
console.error("Clipboard access failed:", err);
|
||||
|
||||
alert(
|
||||
"Unable to access clipboard. Please paste manually using Ctrl+V."
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,24 @@
|
||||
/**
|
||||
* Initializes the download path system once the UI is ready.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Restore last known user-selected download path (localStorage)
|
||||
* - Validate it against backend rules
|
||||
* - Synchronize with backend authoritative path
|
||||
* - Setup folder selection UI interaction
|
||||
*/
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const savePathElem = document.getElementById("savePath");
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// input caché = DERNIER chemin validé par le back
|
||||
/**
|
||||
* Hidden form field storing the validated download path
|
||||
* used during download submission.
|
||||
*
|
||||
* Created dynamically if not already present in DOM.
|
||||
*/
|
||||
let hidden = document.getElementById("savePathInput");
|
||||
|
||||
if (!hidden) {
|
||||
hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
@@ -12,6 +27,17 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
form.appendChild(hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a validated download path received from backend.
|
||||
*
|
||||
* Updates:
|
||||
* - UI display (visible path)
|
||||
* - tooltip (full path)
|
||||
* - hidden form input (used for submission)
|
||||
* - localStorage cache (UX persistence only, not authoritative)
|
||||
*
|
||||
* @param {string} path - validated absolute download path
|
||||
*/
|
||||
async function applyPathFromBack(path) {
|
||||
savePathElem.textContent = path;
|
||||
savePathElem.title = path;
|
||||
@@ -19,6 +45,14 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
localStorage.setItem("customDownloadPath", path); // UX only
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the initial download path on application startup.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. cached localStorage value (UX only, not trusted)
|
||||
* 2. backend validated version of cached path
|
||||
* 3. backend default fallback path if validation fails
|
||||
*/
|
||||
async function loadInitialPath() {
|
||||
// On affiche ce que le user a vu la dernière fois (UX)
|
||||
const cached = localStorage.getItem("customDownloadPath");
|
||||
@@ -42,13 +76,16 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
await loadInitialPath();
|
||||
|
||||
document
|
||||
.getElementById("changePath")
|
||||
.addEventListener("click", async () => {
|
||||
/**
|
||||
* Opens system folder selector and updates download path.
|
||||
*
|
||||
* The selected folder is validated by Electron backend
|
||||
* before being applied to the UI and persisted.
|
||||
*/
|
||||
document.getElementById("changePath").addEventListener("click", async () => {
|
||||
try {
|
||||
// selectDownloadFolder already returns a validated path
|
||||
const validatedPath =
|
||||
await window.electronAPI.selectDownloadFolder();
|
||||
const validatedPath = await window.electronAPI.selectDownloadFolder();
|
||||
|
||||
if (!validatedPath) return; // cancelled
|
||||
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
/**
|
||||
* Currently selected themes loaded from backend.
|
||||
* Used as cache for UI theme switching.
|
||||
*/
|
||||
let loadedThemes = [];
|
||||
|
||||
/**
|
||||
* Applies a theme object to the application UI by mapping
|
||||
* theme configuration values to CSS variables and DOM properties.
|
||||
*
|
||||
* This function mutates:
|
||||
* - document.body (background styling)
|
||||
* - :root CSS variables (design system tokens)
|
||||
* - subtitle DOM element (theme metadata display)
|
||||
*
|
||||
* @param {Object} theme - Theme definition object
|
||||
* @param {Object} theme.style - Style configuration
|
||||
*/
|
||||
function applyTheme(theme) {
|
||||
const root = document.documentElement;
|
||||
const style = theme.style;
|
||||
@@ -15,72 +31,75 @@ function applyTheme(theme) {
|
||||
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 || "");
|
||||
// CSS variables (design tokens)
|
||||
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 || "");
|
||||
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 || "");
|
||||
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-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-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 || "");
|
||||
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 || "");
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists selected theme ID into Electron backend features storage.
|
||||
*
|
||||
* @param {string} themeId
|
||||
*/
|
||||
function saveTheme(themeId) {
|
||||
window.electronAPI.setFeature("theme", themeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates theme dropdown selector with available themes.
|
||||
*
|
||||
* @param {Array} themes
|
||||
*/
|
||||
function populateThemeSelect(themes) {
|
||||
themeSelect.innerHTML = "";
|
||||
|
||||
for (const theme of themes) {
|
||||
const option = document.createElement("option");
|
||||
option.value = theme.id;
|
||||
@@ -89,35 +108,48 @@ function populateThemeSelect(themes) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes theme system:
|
||||
* - loads themes from backend
|
||||
* - restores saved theme
|
||||
* - applies selected theme
|
||||
*/
|
||||
async function initThemes() {
|
||||
loadedThemes = await window.electronAPI.getThemes();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles theme selection change from UI dropdown.
|
||||
*/
|
||||
themeSelect.addEventListener("change", (e) => {
|
||||
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||
if (theme) {
|
||||
applyTheme(theme);
|
||||
saveTheme(theme.id);
|
||||
}
|
||||
if (!theme) return;
|
||||
|
||||
applyTheme(theme);
|
||||
saveTheme(theme.id);
|
||||
});
|
||||
|
||||
/**
|
||||
* Reloads themes from backend and reapplies current selection.
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -2,9 +2,35 @@ const form = document.getElementById("downloadForm");
|
||||
const statusDiv = document.getElementById("downloadStatus");
|
||||
const button = form.querySelector("button[type=\"submit\"]");
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
|
||||
/**
|
||||
* Indicates whether a download process is currently active.
|
||||
* Used to prevent duplicate submissions and to gate UI actions
|
||||
* such as disabling the form or showing progress.
|
||||
*/
|
||||
let isDownloading = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the current download was explicitly
|
||||
* stopped by the user via the cancel action.
|
||||
*
|
||||
* Used to differentiate between:
|
||||
* - user-initiated cancellation
|
||||
* - failures or natural completion
|
||||
*/
|
||||
let userStoppedDownload = false;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles download form submission.
|
||||
*
|
||||
* - Validates user input (URL)
|
||||
* - Disables UI during download
|
||||
* - Sends request to backend `/download`
|
||||
* - Manages success / error states
|
||||
* - Restores UI state after completion
|
||||
*/
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -61,6 +87,13 @@ form.addEventListener("submit", async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays initial download progress UI immediately
|
||||
* when a download starts.
|
||||
*
|
||||
* Initializes progress bar, speed indicator, stage text
|
||||
* and shows the stop button.
|
||||
*/
|
||||
function showInitialProgress() {
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
const progressBarText = document.getElementById("downloadProgressText");
|
||||
@@ -84,6 +117,14 @@ function showInitialProgress() {
|
||||
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current download process.
|
||||
*
|
||||
* Sends a request to backend `/download/cancel`,
|
||||
* updates UI state and resets progress indicators.
|
||||
*
|
||||
* Prevents duplicate calls if no download is active.
|
||||
*/
|
||||
async function stopDownload() {
|
||||
if (!isDownloading) return;
|
||||
|
||||
@@ -109,6 +150,12 @@ async function stopDownload() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all download progress UI elements to their
|
||||
* default hidden state.
|
||||
*
|
||||
* Used after completion or cancellation of a download.
|
||||
*/
|
||||
function resetProgressBar() {
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
if (progressWrapper) progressWrapper.style.display = "none";
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
/**
|
||||
* Converts a raw upload date string (format YYYYMMDD)
|
||||
* into a human-readable format (DD/MM/YYYY).
|
||||
*
|
||||
* Returns "Inconnue" if the input is invalid or malformed.
|
||||
*/
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file size in bytes into megabytes (MB).
|
||||
*
|
||||
* Returns a formatted string with 2 decimals (e.g. "12.34 Mo").
|
||||
* If input is falsy, returns "Inconnue".
|
||||
*/
|
||||
function formatSize(bytes) {
|
||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches video or playlist metadata from the backend `/info` endpoint.
|
||||
*
|
||||
* Sends a POST request with URL-encoded body.
|
||||
* Handles network errors, HTTP errors and JSON parsing safely.
|
||||
*
|
||||
* @param {string} url - Video or playlist URL
|
||||
* @returns {Promise<Object>} Parsed metadata or error object
|
||||
*/
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
@@ -26,9 +47,19 @@ async function fetchVideoInfo(url) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the video info system UI.
|
||||
*
|
||||
* - Listens to URL input changes
|
||||
* - Automatically fetches video/playlist metadata if enabled
|
||||
* - Renders either playlist UI or single video UI
|
||||
* - Handles loading states and caching (lastFetchedUrl)
|
||||
*
|
||||
* Also manages dynamic playlist title injection into the form.
|
||||
*/
|
||||
async function init() {
|
||||
|
||||
// Main input listener: triggers auto-fetch when URL changes
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
|
||||
@@ -1,40 +1,65 @@
|
||||
/**
|
||||
* DOM elements used for download progress UI rendering.
|
||||
* These elements are updated in real-time via SSE streams.
|
||||
*/
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
const progressBar = document.getElementById("downloadProgress");
|
||||
const progressBarText = document.getElementById("downloadProgressText")
|
||||
const progressBarText = document.getElementById("downloadProgressText");
|
||||
|
||||
const speedElement = document.getElementById("downloadSpeedText");
|
||||
const stageElement = document.getElementById("downloadStage");
|
||||
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||
|
||||
/**
|
||||
* Server-Sent Events streams for real-time download updates.
|
||||
* Each stream is responsible for a specific aspect of the download state.
|
||||
*/
|
||||
const speedEvt = new EventSource("/download/speed");
|
||||
const stageEvt = new EventSource("/download/stage");
|
||||
const playlistInfoEvt = new EventSource("/download/playlist-info");
|
||||
|
||||
/**
|
||||
* Resets the progress UI to its initial state.
|
||||
* Hides all dynamic elements and clears displayed values.
|
||||
*/
|
||||
function startProgress() {
|
||||
progressBar.style.width = "0%";
|
||||
progressBarText.innerHTML = "0%";
|
||||
speedElement.innerHTML = "0 Mib/s";
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the progress UI visible and enables related controls
|
||||
* such as the stop button.
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress bar UI with the given percentage.
|
||||
* Automatically triggers UI visibility if not already shown.
|
||||
*
|
||||
* @param {number} percent - Download progress percentage (0–100)
|
||||
*/
|
||||
function updateProgress(percent) {
|
||||
// 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}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully resets the progress UI and hides all download indicators.
|
||||
* Called after completion or cancellation.
|
||||
*/
|
||||
function resetProgress() {
|
||||
progressBar.style.width = "0%";
|
||||
progressBarText.textContent = "";
|
||||
@@ -50,13 +75,19 @@ function resetProgress() {
|
||||
playlistInfoElement.textContent = "";
|
||||
playlistInfoElement.style.display = "none";
|
||||
|
||||
// Hide stop button
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
if (stopBtn) stopBtn.style.display = "none";
|
||||
}
|
||||
|
||||
// SSE Connexion
|
||||
/**
|
||||
* Main SSE stream handling download progress updates.
|
||||
* Supports:
|
||||
* - reset signal
|
||||
* - done signal
|
||||
* - percentage updates
|
||||
*/
|
||||
const evtSource = new EventSource("/download/progress");
|
||||
|
||||
evtSource.onmessage = e => {
|
||||
if (e.data === "reset") {
|
||||
startProgress();
|
||||
@@ -65,34 +96,41 @@ evtSource.onmessage = e => {
|
||||
}
|
||||
|
||||
if (e.data === "done") {
|
||||
// Keep progress bar visible for better UX, let toast handle the feedback
|
||||
setTimeout(() => {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1);
|
||||
}, 2000); // Wait 2s so user sees 100% progress
|
||||
window.electronAPI.setProgress(-1);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const percent = parseFloat(e.data);
|
||||
|
||||
|
||||
if (!isNaN(percent)) {
|
||||
updateProgress(percent);
|
||||
window.electronAPI.setProgress(percent); // Update Task Bar
|
||||
// Don't hide at 100%, wait for "done" signal instead
|
||||
window.electronAPI.setProgress(percent);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* SSE stream: download speed updates (e.g. "5.2 MiB/s").
|
||||
*/
|
||||
speedEvt.onmessage = e => {
|
||||
speedElement.style.display = "block";
|
||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||
speedElement.textContent = e.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* SSE stream: download stage updates (e.g. downloading, merging, extracting).
|
||||
*/
|
||||
stageEvt.onmessage = e => {
|
||||
stageElement.style.display = "block";
|
||||
stageElement.textContent = e.data; // ex: "📥 Downloading..."
|
||||
stageElement.textContent = e.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* SSE stream: playlist progress updates (e.g. "Item 1 of 15").
|
||||
*/
|
||||
playlistInfoEvt.onmessage = e => {
|
||||
playlistInfoElement.style.display = "block";
|
||||
playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15"
|
||||
playlistInfoElement.textContent = e.data;
|
||||
};
|
||||
@@ -1,66 +1,105 @@
|
||||
/**
|
||||
* Settings panel DOM elements used to control application feature flags
|
||||
* and configuration UI interactions.
|
||||
*/
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const settingsModal = document.querySelector(".settings-modal");
|
||||
|
||||
const settingsButton = document.getElementById("settings-button");
|
||||
|
||||
/**
|
||||
* Tracks current settings panel state.
|
||||
* NOTE: actual visibility is derived from DOM style in current implementation.
|
||||
*/
|
||||
let isOpen = false;
|
||||
|
||||
/**
|
||||
* Toggles visibility of the settings panel when clicking the settings button.
|
||||
*/
|
||||
settingsButton.addEventListener("click", () => {
|
||||
const isOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
||||
const currentlyOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = currentlyOpen ? "none" : "block";
|
||||
});
|
||||
|
||||
/**
|
||||
* Closes the settings panel and resets internal state.
|
||||
*/
|
||||
function closePanel() {
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads feature flags from Electron backend and syncs them
|
||||
* with corresponding UI controls (checkboxes, selects).
|
||||
*
|
||||
* Also registers change listeners to persist updates in real time.
|
||||
*/
|
||||
async function loadSettings() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
||||
const key = el.dataset.key;
|
||||
if (features[key] !== undefined) {
|
||||
if (el.type === "checkbox") el.checked = features[key];
|
||||
else if (el.tagName === "SELECT") el.value = features[key];
|
||||
}
|
||||
document
|
||||
.querySelectorAll(".settings-section input, .settings-section select")
|
||||
.forEach((el) => {
|
||||
const key = el.dataset.key;
|
||||
if (!key) return;
|
||||
|
||||
el.addEventListener("change", () => {
|
||||
if (key === "theme") return;
|
||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||
window.electronAPI.setFeature(key, value);
|
||||
if (features[key] !== undefined) {
|
||||
if (el.type === "checkbox") el.checked = features[key];
|
||||
else if (el.tagName === "SELECT") el.value = features[key];
|
||||
}
|
||||
|
||||
el.addEventListener("change", () => {
|
||||
if (key === "theme") return;
|
||||
|
||||
const value = el.type === "checkbox" ? el.checked : el.value;
|
||||
window.electronAPI.setFeature(key, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Open The JSON
|
||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||
window.topbarAPI.openConfig();
|
||||
/**
|
||||
* Opens configuration JSON file via Electron topbar API.
|
||||
*/
|
||||
document.getElementById("open-json-btn")
|
||||
?.addEventListener("click", () => {
|
||||
window.topbarAPI.openConfig();
|
||||
});
|
||||
|
||||
/**
|
||||
* Opens theme directory via Electron topbar API.
|
||||
*/
|
||||
document.getElementById("open-theme")
|
||||
?.addEventListener("click", () => {
|
||||
window.topbarAPI.openTheme();
|
||||
});
|
||||
|
||||
/**
|
||||
* Refreshes theme list and updates UI selector with a short animation.
|
||||
*/
|
||||
document.getElementById("refresh-themes-btn")
|
||||
?.addEventListener("click", function () {
|
||||
this.classList.add("spinning");
|
||||
window.refreshThemes();
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove("spinning");
|
||||
}, 600);
|
||||
});
|
||||
|
||||
/**
|
||||
* Clicking the overlay outside the modal closes the settings panel.
|
||||
*/
|
||||
settingsPanel?.addEventListener("click", closePanel);
|
||||
|
||||
/**
|
||||
* Prevents modal clicks from closing the settings panel
|
||||
* by stopping event propagation to the overlay.
|
||||
*/
|
||||
settingsModal?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
||||
/* clic sur l'overlay = fermer */
|
||||
settingsPanel.addEventListener("click", closePanel);
|
||||
|
||||
/* clic dans la modal = stop propagation */
|
||||
settingsModal.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
/**
|
||||
* Initializes settings UI by loading backend feature flags.
|
||||
*/
|
||||
loadSettings();
|
||||
@@ -1,17 +1,26 @@
|
||||
window.showToast = function(message, type = 'info', duration = 4000) {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
/**
|
||||
* Displays a toast notification in the global UI container.
|
||||
*
|
||||
* Toasts are automatically appended to `#toast-container`,
|
||||
* support multiple visual types, and can auto-dismiss after a delay.
|
||||
*
|
||||
* @param {string} message - Message displayed inside the toast
|
||||
* @param {"success"|"error"|"warning"|"info"} [type="info"] - Visual style of the toast
|
||||
* @param {number} [duration=4000] - Auto-dismiss delay in milliseconds (0 disables auto removal)
|
||||
* @returns {HTMLElement|null} The created toast element or null if container is missing
|
||||
*/
|
||||
window.showToast = function (message, type = "info", duration = 4000) {
|
||||
const container = document.getElementById("toast-container");
|
||||
if (!container) return null;
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.classList.add('toast', type);
|
||||
const toast = document.createElement("div");
|
||||
toast.classList.add("toast", type);
|
||||
|
||||
// Icons for different types
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
success: "✓",
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
info: "ℹ"
|
||||
};
|
||||
|
||||
const icon = icons[type] || icons.info;
|
||||
@@ -22,14 +31,11 @@ window.showToast = function(message, type = 'info', duration = 4000) {
|
||||
<button class="toast-close" title="Close">×</button>
|
||||
`;
|
||||
|
||||
// Add close functionality
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => removeToast(toast));
|
||||
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);
|
||||
}
|
||||
@@ -37,18 +43,41 @@ window.showToast = function(message, type = 'info', duration = 4000) {
|
||||
return toast;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a toast element with a small exit animation.
|
||||
*
|
||||
* @param {HTMLElement} toast
|
||||
*/
|
||||
function removeToast(toast) {
|
||||
if (!toast.parentElement) return;
|
||||
|
||||
toast.classList.add('removing');
|
||||
if (!toast?.parentElement) return;
|
||||
|
||||
toast.classList.add("removing");
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
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);
|
||||
/**
|
||||
* Shortcut: success toast
|
||||
*/
|
||||
window.showSuccess = (message, duration = 4000) =>
|
||||
window.showToast(message, "success", duration);
|
||||
|
||||
/**
|
||||
* Shortcut: error toast
|
||||
*/
|
||||
window.showError = (message, duration = 5000) =>
|
||||
window.showToast(message, "error", duration);
|
||||
|
||||
/**
|
||||
* Shortcut: warning toast
|
||||
*/
|
||||
window.showWarning = (message, duration = 4000) =>
|
||||
window.showToast(message, "warning", duration);
|
||||
|
||||
/**
|
||||
* Shortcut: info toast
|
||||
*/
|
||||
window.showInfo = (message, duration = 4000) =>
|
||||
window.showToast(message, "info", duration);
|
||||
@@ -1,16 +1,22 @@
|
||||
// Ajout des listeners pour la topbar
|
||||
/**
|
||||
* Initializes topbar button event listeners and binds them
|
||||
* to the Electron preload API exposed in `window.topbarAPI`.
|
||||
*
|
||||
* This function is safe to call once DOM is ready.
|
||||
* It silently aborts if the API or elements are missing.
|
||||
*/
|
||||
function setupTopbarListeners() {
|
||||
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');
|
||||
|
||||
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();
|
||||
@@ -22,12 +28,30 @@ function setupTopbarListeners() {
|
||||
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
|
||||
/**
|
||||
* Registers topbar event listeners after DOM is fully loaded.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
/**
|
||||
* Applies UI layout adjustments depending on feature flags
|
||||
* provided by the Electron backend.
|
||||
*
|
||||
* Specifically handles:
|
||||
* - hiding custom topbar
|
||||
* - adjusting layout spacing
|
||||
* - repositioning theme switcher
|
||||
*/
|
||||
(async function applyFeatureLayout() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if(!features.customTopBar){
|
||||
document.getElementById("topbar").style.display = "none";
|
||||
document.getElementById("container").style.marginTop = "0";
|
||||
document.getElementById("theme-switcher").style.top = "30px";
|
||||
}
|
||||
if (!features.customTopBar) {
|
||||
const topbar = document.getElementById("topbar");
|
||||
const container = document.getElementById("container");
|
||||
const themeSwitcher = document.getElementById("theme-switcher");
|
||||
|
||||
if (topbar) topbar.style.display = "none";
|
||||
if (container) container.style.marginTop = "0";
|
||||
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user