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:
MasterAcnolo
2026-05-31 20:38:42 +02:00
parent 6afc5e06d6
commit 0218664c5a
34 changed files with 1777 additions and 340 deletions

View File

@@ -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();

View File

@@ -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."
);
}
});
});
});

View File

@@ -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

View File

@@ -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;

View File

@@ -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";

View File

@@ -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");

View File

@@ -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 (0100)
*/
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;
};

View File

@@ -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();

View File

@@ -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);

View File

@@ -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";
}
})();