diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..4008de78b4c6cf5d9668e7ed38d9779e0809cf97 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Lucie Bader, Alissa Sounalath + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7ef22536e005fce9ee5f816852a50a1578d273c6..435b323a3866364c6335a1e05904d90bd546fb7d 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,26 @@ -# balex2ff -Un projet de plugin firefox qui interagirait avec [balex](https://balex.liris.cnrs.fr/)… +# Extension Firefox - ff2balex +Un projet de plugin Firefox qui interagit avec [BaLex](https://balex.liris.cnrs.fr/). Ce projet s'inscrit dans la continuité du projet [Lex:gaMe](https://aslan.universite-lyon.fr/projet-lex-game-233220.kjsp). -## Resources -* [solution pour échanger avec un script python](https://developer.mozilla.org/fr/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) -* [a-t-on besoin de scripts de contenu ? — probablement que oui](https://developer.mozilla.org/fr/docs/Mozilla/Add-ons/WebExtensions/Content_scripts) +Toutes les informations sur **le fonctionnement et l'utilisation** de cette extension sont disponibles à partir de [cette page](https://gitlab.liris.cnrs.fr/lex-game/balex2ff/-/wikis/home). -## Wiki Lex:gaMe -> https://wiki.lezinter.net/_/LexgaMe:Ff2balex +L'**installation** est à ce jour manuelle. La procédure est détaillée [ici](https://gitlab.liris.cnrs.fr/lex-game/balex2ff/-/wikis/Documentation/Utilisation-de-l'extension/Installation). -## Licence -> MIT ? à définir -Plug-in réalisé dans le cadre du projet [Lex:gaMe](https://aslan.universite-lyon.fr/projet-lex-game-233220.kjsp) +### Ressources +* [Solution pour échanger avec un script python](https://developer.mozilla.org/fr/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) +* [A-t-on besoin de scripts de contenu ? — probablement que oui](https://developer.mozilla.org/fr/docs/Mozilla/Add-ons/WebExtensions/Content_scripts) -## Installation de l'extension (module complémentaire temporaire) -- Télécharger le dossier 'menu_extension' -- Ouvrir Firefox et accéder aux modules complémentaires (Ctrl + Shift + A (Windows/Linux) ou Cmd + Shift + A (Mac)) -- Cliquer sur l'icône en haut à droite puis sur "Déboguer des modules" -- Cliquer sur "Charger un module complémentaire temporaire" -- Sélectionner le fichier 'manifest.json' -- Pour veiller au bon fonctionnement du worker de l'extension, désactiver l'option "Utiliser toujours le mode de navigation privée" voir [lien](https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#service-workers-not-compatible). +### Crédits +* [`link_to_balex.svg`](/src/assets/icons/Link_to_balex.svg) MIT https://www.svgrepo.com/svg/450128/external-link-s +* TODO : mettre les autres crédits (dépendances — ex pyodide, images, fonts) -## Connexion/Déconnexion de l'utilisateur via le menu de l'extension -Via un bouton de connexion/déconnexion dans le menu de l'extension, l'utilisateur accède à la page d'authentification de [Prisms](https://prisms.lezinter.net/fr/login), complète ses informations de connexion et doit cliquer sur "Se connecter avec BaLex". Le script récupère ensuite le token d'authentification et le stocke dans le LocalStorage. -Si l'utilisateur clique sur le bouton "Se déconnecter" de l'extension, le token est supprimé. -## Activation des statistiques : -Cliquer sur le bouton "Activer les statistiques". -Lorsqu’un utilisateur se connecte, il peut activer l'ajout automatique des mots dans les lexiques personnels sélectionnés et fixer un seuil d'ajout (Cliquer sur le bouton "Valider" après avoir fixé un seuil et sélectionné au moins une langue). +**Police utilisée : Luciole** +Conçue spécifiquement pour les personnes malvoyantes. +© Laurent Bourcellier & Jonathan Perez – Distribuée gratuitement sous la [Licence Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode.fr). +Source : [Luciole Vision](https://luciole-vision.com/) + + +**Icônes** +Création des icônes d'options sur [icones8](https://icones8.fr/icons) -## Crédits -* [`link_to_balex.svg`](/src/assets/icons/Link_to_balex.svg) MIT https://www.svgrepo.com/svg/450128/external-link-s -* TODO : mettre les autres crédits (dépendances — ex pyodide, images, fonts) diff --git a/manifest.json b/manifest.json index d465f43555b8ab019dc62e2f4ffae2a8cba59b1c..5817a844c640730cf92f218f1a8d4ba04e8302a2 100644 --- a/manifest.json +++ b/manifest.json @@ -31,18 +31,13 @@ "browser_action": { "default_area": "navbar", - "default_popup": "src/popup/popup.html", + "default_popup": "src/plugin/plugin.html", "default_icon": { "16": "src/assets/icons/logo.png" }, "default_title": "ff2BaLex" }, - "options_ui": { - "page": "src/options/options.html", - "open_in_tab": false - }, - "sidebar_action": { "default_title": "BaLex", "default_panel": "src/sidebar/sidebar.html", @@ -61,14 +56,28 @@ "src/utils/definitions.js", "src/sidebar/sidebar.js", "src/context_menu/custom_context_menu.js", - "src/utils/stats.js"], - "css": ["src/context_menu/custom_context_menu.css"], + "src/utils/stats.js", + "src/utils/highlighting.js" + ], + "css": ["src/css/custom_context_menu.css"], "run_at": "document_idle" }, { "matches": ["<all_urls>"], - "js": ["src/utils/stats.js"], + "js": [ + "src/utils/logger.js", + "src/utils/stats.js" + ], "run_at": "document_end" + }, + { + "matches": ["<all_urls>"], + "js": [ + "src/utils/logger.js", + "src/utils/api.js", + "src/utils/highlighting.js" + ], + "run_at": "document_start" } ], diff --git a/src/assets/lexicon_icon.js b/src/assets/lexicon_icon.js index 1247bcfbdf8386aea0a1163e2cf950fddb466a1e..fa5852f7f7fc4b8b445bfa13c54d910626c0c585 100644 --- a/src/assets/lexicon_icon.js +++ b/src/assets/lexicon_icon.js @@ -1,6 +1,5 @@ - /** - * Récupère les couleurs des lexiques de l’utilisateur. + * Récupère les couleurs des lexiques de l'utilisateur. * Chaque lexique est censé posséder une propriété "rGB" contenant la couleur, par exemple "65, 148, 84". * * @param {string} authToken - Le token d'authentification. @@ -13,13 +12,13 @@ async function getLexiconsColors(authToken) { const lexicons = await callApi(url, authToken); const colors = {}; lexicons.forEach(lexicon => { - colors[lexicon.id] = lexicon.rGB; + colors[lexicon.id] = lexicon.rGB; // Associe l'ID du lexique à sa couleur rGB }); - log("✅ Couleurs des lexiques récupérées :", colors); - return colors; + log("Couleurs des lexiques récupérées :", colors); + return colors; // Retourne l'objet contenant les couleurs des lexiques } catch (error) { - log("⌠Erreur lors de la récupération des couleurs des lexiques :", error); + log("Erreur lors de la récupération des couleurs des lexiques :", error); return {}; } } @@ -31,41 +30,9 @@ async function getLexiconsColors(authToken) { */ function convertColor(rgbString) { const parts = rgbString.split(',').map(part => parseInt(part.trim(), 10)); - // if (parts.length !== 3 || parts.some(isNaN)) { - // return generateRandomColor(); - // } - return "#" + parts.map(n => n.toString(16).padStart(2, '0')).join(''); + return "#" + parts.map(n => n.toString(16).padStart(2, '0')).join(''); // Convertit en hexadécimal } -/** - * Sélectionne aléatoirement une couleur dans une palette prédéfinie. - * @returns {string} Une couleur au format hexadécimal ou HSL. - */ -// function generateRandomColor() { -// const palette = [ -// "#231942", -// "#5E548E", -// "#9F86C0", -// "#BE95C4", -// "#E0B1CB", -// "#b7094c", -// "#a01a58", -// "#892b64", -// "#723c70", -// "#5b4d7c", -// "#455e89", -// "#2e6f95", -// "#1780a1", -// "#0091ad", -// "#30343f", -// "#e4d9ff", -// "#273469", -// "#1e2749" -// ]; -// const index = Math.floor(Math.random() * palette.length); -// return palette[index]; -// } - /** * Obtient (ou crée) la couleur associée à un lexique donné en utilisant browser.storage.local. * @param {string|number} lexiconId - L'identifiant du lexique. @@ -75,7 +42,7 @@ async function getOrCreateLexiconColor(lexiconId) { // Récupère la correspondance stockée dans storage let { lexiconColors } = await browser.storage.local.get("lexiconColors"); if (!lexiconColors || forceReset) { - lexiconColors = {}; + lexiconColors = {}; // Initialise un nouvel objet si aucune couleur n'est trouvée } if (window.authToken) { try { @@ -83,19 +50,14 @@ async function getOrCreateLexiconColor(lexiconId) { // Pour chaque lexique récupéré depuis l'API, on convertit la couleur rGB en hexadécimal for (const id in apiColors) { if (Object.prototype.hasOwnProperty.call(apiColors, id)) { - lexiconColors[id] = convertColor(apiColors[id]); + lexiconColors[id] = convertColor(apiColors[id]); // Stocke la couleur convertie } } } catch (error) { - log("Erreur lors de la récupération des couleurs via l'API :", error); + log("Erreur lors de la récupération des couleurs via l'API :", error); } } - // Si aucune couleur n'est associée, on la génère et on la sauvegarde - // if (!lexiconColors[lexiconId]) { - // lexiconColors[lexiconId] = generateRandomColor(); - // await browser.storage.local.set({ lexiconColors }); - // } - return lexiconColors[String(lexiconId)]; + return lexiconColors[String(lexiconId)]; // Retourne la couleur associée au lexique } /** @@ -126,14 +88,14 @@ async function updateLexiconColors(authToken) { const colorMapping = {}; for (const id in apiColors) { if (Object.prototype.hasOwnProperty.call(apiColors, id)) { - colorMapping[id] = convertColor(apiColors[id]); + colorMapping[id] = convertColor(apiColors[id]); // Convertit et stocke la couleur } } - log("✅ Mise à jour des couleurs des lexiques :", colorMapping); - await browser.storage.local.set({ lexiconColors: colorMapping }); - return colorMapping; + log("Mise à jour des couleurs des lexiques :", colorMapping); + await browser.storage.local.set({ lexiconColors: colorMapping }); // Met à jour le stockage local + return colorMapping; // Retourne la map des couleurs } catch (error) { - log("⌠Erreur lors de la mise à jour des couleurs :", error); + log("Erreur lors de la mise à jour des couleurs :", error); return {}; } } @@ -147,6 +109,7 @@ async function getColorForLexicon(lexiconId) { const { lexiconColors } = await browser.storage.local.get("lexiconColors"); return (lexiconColors && lexiconColors[String(lexiconId)]) || "#cccccc"; } + /** * Convertit une couleur hexadécimale en une couleur RGBA. * @param {string} hex - La couleur en hexadécimal. @@ -161,9 +124,9 @@ function hexToRgba(hex, opacity) { return `rgba(${r}, ${g}, ${b}, ${opacity})`; } +// Expose les fonctions globalement window.updateLexiconColors = updateLexiconColors; window.getColorForLexicon = getColorForLexicon; -// window.generateRandomColor = generateRandomColor; window.convertColor = convertColor; window.getOrCreateLexiconColor = getOrCreateLexiconColor; window.createColorCircle = createColorCircle; diff --git a/src/background/background.js b/src/background/background.js index b41da8b821d6f3034254aa6808c98b90c8b74337..d6a4be6bbd3881076d0e50f3e7e09f39ebabad23 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -1,55 +1,60 @@ // ───────────────────────────────────────────────────────────────────────────── // Variables globales // ───────────────────────────────────────────────────────────────────────────── -let isExtensionActive = true; -let areStatsActive = false; -let originalTabId = null; -let loginTabId = null; +let isExtensionActive = true; // Indique si l'extension est active +let areStatsActive = false; // Indique si le suivi des statistiques est actif +let originalTabId = null; // ID de l'onglet original +let loginTabId = null; // ID de l'onglet de connexion -const AUTH_LOGIN_URL = "https://prisms.lezinter.net/fr/login"; -const AUTH_BALEX_URL = "https://prisms.lezinter.net/fr/headquarters/balex"; +const AUTH_LOGIN_URL = "https://prisms.lezinter.net/fr/login"; // URL de connexion +const AUTH_BALEX_URL = "https://prisms.lezinter.net/fr/headquarters/balex"; // URL de redirection vers BaLex // ───────────────────────────────────────────────────────────────────────────── // Logs de démarrage et initialisation // ───────────────────────────────────────────────────────────────────────────── -log("🚀 ff2BaLex (background) chargé."); - +log("ff2BaLex (background) chargé."); browser.runtime.onInstalled.addListener((details) => { - log("🔔 Extension installée ou mise à jour. Raison :", details.reason); + log("Extension installée ou mise à jour. Raison :", details.reason); }); - browser.runtime.onStartup.addListener(() => { - log("🔄 Extension démarrée (onStartup)."); + log("Extension démarrée (onStartup)."); }); - browser.runtime.onInstalled.addListener(() => { browser.storage.local.set({ extensionActive: false }); - log("🔔 Extension installée, état initialisé à désactivé."); + log("Extension installée, état initialisé à désactivé."); }); // ───────────────────────────────────────────────────────────────────────────── // Suivi des changements dans le stockage // ───────────────────────────────────────────────────────────────────────────── browser.storage.onChanged.addListener((changes) => { + // Vérifie si l'état de l'extension a changé if (changes.extensionActive) { - isExtensionActive = changes.extensionActive.newValue; - log("✅ Extension activée :", isExtensionActive); + isExtensionActive = changes.extensionActive.newValue; // Met à jour la variable d'état de l'extension + log("Extension activée :", isExtensionActive); } + // Vérifie si l'état des statistiques a changé if (changes.statsActive) { - areStatsActive = changes.statsActive.newValue; - log("📊 Statistiques activées :", areStatsActive); + areStatsActive = changes.statsActive.newValue; // Met à jour la variable d'état des statistiques + log("Statistiques activées :", areStatsActive); } + // Rafraîchit l'interface utilisateur globale refreshAllUI(); }); browser.storage.onChanged.addListener((changes, area) => { + // Vérifie si les changements concernent le stockage local et le token d'accès if (area === "local" && changes.accessToken) { - const newToken = changes.accessToken.newValue; + const newToken = changes.accessToken.newValue; // Récupère la nouvelle valeur du token if (newToken) { + // Vérifie l'état de l'extension dans le stockage local browser.storage.local.get("extensionActive").then(({ extensionActive }) => { + // Si l'extension n'est pas active, l'active automatiquement if (!extensionActive) { log("Token ajouté, activation automatique de l'extension."); - browser.storage.local.set({ extensionActive: true }); + browser.storage.local.set({ extensionActive: true }); // Met à jour l'état de l'extension + updateExtension(); // Met à jour les fonctionnalités de l'extension + // Envoie un message pour mettre à jour l'interface utilisateur browser.runtime.sendMessage({ action: "updateUI", extensionActive: true, @@ -65,13 +70,22 @@ browser.storage.onChanged.addListener((changes, area) => { // ───────────────────────────────────────────────────────────────────────────── // Fonctions utilitaires // ───────────────────────────────────────────────────────────────────────────── +/** + * Vérifie si l'utilisateur est connecté en vérifiant le token d'accès. + * @returns {Promise<boolean>} True si l'utilisateur est connecté, sinon false. + */ async function isUserConnected() { const { accessToken } = await browser.storage.local.get("accessToken"); return !!accessToken; } +/** + * Rafraîchit l'interface utilisateur globale. + * Envoie un message pour demander le rafraîchissement de l'UI. + * @returns {Promise<void>} + */ async function refreshAllUI() { - log("🔄 Rafraîchissement global de l'UI..."); + log("Rafraîchissement global de l'UI..."); try { await browser.runtime.sendMessage({ action: "refreshUI" }); } catch (error) { @@ -86,44 +100,53 @@ browser.runtime.onConnect.addListener((port) => { if (port.name === "auth") { port.onMessage.addListener(async (message) => { if (message.action === "toggleAuth") { - log("🔄 toggleAuth reçu via port dans le background."); - const isConnected = await isUserConnected(); + log("toggleAuth reçu via port dans le background."); + const isConnected = await isUserConnected(); // Vérifie si l'utilisateur est connecté if (isConnected) { - await disconnectFromLexicalDB(); + await disconnectFromLexicalDB(); // Déconnecte l'utilisateur } else { - actuallyOpenLoginPage(); + actuallyOpenLoginPage(); // Ouvre la page de connexion } } }); } }); +/** + * Ouvre la page de connexion pour l'utilisateur. + * Mémorise l'onglet actif et crée un nouvel onglet pour la connexion. + * @returns {Promise<void>} + */ async function actuallyOpenLoginPage() { - log("🔗 Ouverture de la page de connexion."); + log("Ouverture de la page de connexion."); // Mémoriser l'onglet actif const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true }); if (currentTab) { - originalTabId = currentTab.id; - log("✅ Onglet courant mémorisé, ID =", originalTabId); + originalTabId = currentTab.id; // Mémorise l'ID de l'onglet courant + log("Onglet courant mémorisé, ID =", originalTabId); } // Ouvre un nouvel onglet pour la page de connexion et l'active const loginTab = await browser.tabs.create({ url: AUTH_LOGIN_URL, active: true }); - loginTabId = loginTab.id; - log("✅ Onglet de login créé, ID =", loginTabId); - // Notifie que l'authentification est en cours + loginTabId = loginTab.id; // Mémorise l'ID de l'onglet de connexion + log("Onglet de login créé, ID =", loginTabId); browser.runtime.sendMessage({ action: "authStatusChanged", isLoggedIn: false }); } +/** + * Déconnecte l'utilisateur. + * Supprime le token d'accès et désactive l'extension. + * @returns {Promise<void>} + */ async function disconnectFromLexicalDB() { - await browser.storage.local.remove("accessToken"); - log("🔓 Token supprimé avec succès."); + await browser.storage.local.remove("accessToken"); // Supprime le token d'accès + log("Token supprimé avec succès."); - await browser.storage.local.remove("lexiconColors"); + await browser.storage.local.remove("lexiconColors"); // Supprime les couleurs du lexique // Désactivation automatique de l'extension - await browser.storage.local.set({ extensionActive: false }); - disableExtensionFeatures(); + await browser.storage.local.set({ extensionActive: false }); // Met à jour l'état de l'extension + disableExtensionFeatures(); // Désactive les fonctionnalités de l'extension browser.runtime.sendMessage({ action: "updateUI", extensionActive: false, @@ -132,37 +155,43 @@ async function disconnectFromLexicalDB() { }); setTimeout(async () => { - await refreshAllUI(); + await refreshAllUI(); // Rafraîchit l'UI après un délai }, 500); } +/** + * Sauvegarde le token d'accès dans le stockage local. + * @param {string} token - Le token à sauvegarder. + * @returns {Promise<void>} + */ async function saveToken(token) { - log("✅ Sauvegarde du token :", token); + log("Sauvegarde du token :", token); await browser.storage.local.set({ accessToken: token }); if (loginTabId) { try { - await browser.tabs.remove(loginTabId); - log("🗙 Onglet de login fermé après connexion réussie."); + await browser.tabs.remove(loginTabId); // Ferme l'onglet de connexion + log("Onglet de login fermé après connexion réussie."); } catch (err) { console.warn("Impossible de fermer l'onglet de login :", err); } - loginTabId = null; + loginTabId = null; // Réinitialise l'ID de l'onglet de connexion } if (originalTabId) { try { - await browser.tabs.update(originalTabId, { active: true }); - log("🔙 Retour sur l'onglet initial :", originalTabId); + await browser.tabs.update(originalTabId, { active: true }); // Retourne sur l'onglet initial + log("Retour sur l'onglet initial :", originalTabId); } catch (err) { console.warn("Impossible de basculer sur l'onglet initial :", err); } - originalTabId = null; + originalTabId = null; // Réinitialise l'ID de l'onglet original } // Activer automatiquement l'extension const { extensionActive } = await browser.storage.local.get("extensionActive"); if (!extensionActive) { - await browser.storage.local.set({ extensionActive: true }); + await browser.storage.local.set({ extensionActive: true }); // Met à jour l'état de l'extension + updateExtension(); // Met à jour les fonctionnalités de l'extension browser.runtime.sendMessage({ action: "updateUI", extensionActive: true, @@ -173,136 +202,54 @@ async function saveToken(token) { await refreshAllUI(); } -// ───────────────────────────────────────────────────────────────────────────── -// Gestion des messages reçus -// ───────────────────────────────────────────────────────────────────────────── -browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => { - log("📩 Message reçu dans background.js :", message); - - switch (message.action) { - case "toggleAuth": { - const isConnected = await isUserConnected(); - if (isConnected) { - await disconnectFromLexicalDB(); - await browser.storage.local.remove("lexiconColors"); - } else { - actuallyOpenLoginPage(); - } - break; - } - case "getDefinitionWiki": { - if (message.selectedText && message.selectedText.trim() !== "") { - log("🌠Requête Wiktionnaire pour :", message.selectedText); - const definition = await window.fetchWiktionaryDefinition(message.selectedText.trim()); - browser.runtime.sendMessage({ - action: "fetchWiktionaryDefinitionResponse", - selectedText: message.selectedText, - definitions: [{ - source: "Wiktionnaire", - text: definition, - }], - }); - } else { - console.warn("âš ï¸ Texte sélectionné vide. Annulation de la requête."); - } - break; - } - case "checkAuthStatus": { - const connected = await isUserConnected(); - sendResponse(connected); - break; - } - case "authStatusChanged": { - log("🔄 Mise à jour de l'état d'authentification :", message.isLoggedIn); - break; - } - case "saveToken": { - if (message.token) { - await saveToken(message.token); - } else { - console.warn("âš ï¸ Aucune valeur de token reçue."); - } - break; - } - case "toggleLexiconHighlight": { - const tabs = await browser.tabs.query({active: true, currentWindow: true}); - if (tabs[0]) { - try { - // S'assurer que le script est injecté - await browser.scripting.executeScript({ - target: { tabId: tabs[0].id }, - files: ["src/utils/highlighting.js"] - }); - - // Envoyer le message d'activation - await browser.tabs.sendMessage(tabs[0].id, { - command: message.isActive ? "activate-highlighting" : "deactivate-highlighting", - lexiconId: message.lexiconId - }); - log(`✅ Message de surlignage transmis à l'onglet ${tabs[0].id}`); - } catch (error) { - log("⌠Erreur lors de la gestion du surlignage:", error); - } - } - break; - } - - case "register-highlighting-script": { - log("📠Script de surlignage enregistré pour l'onglet", sender.tab.id); - break; - } - - default: - break; - } - return true; -}); - -// ───────────────────────────────────────────────────────────────────────────── -// Web Navigation : Injection de scripts et récupération du token -// ───────────────────────────────────────────────────────────────────────────── +/** + * Gère les événements de navigation web pour injecter des scripts et récupérer le token. + * @param {Object} details - Détails de l'événement de navigation. + * @returns {Promise<void>} + */ browser.webNavigation.onCompleted.addListener(async (details) => { const url = new URL(details.url); // Injection d'un popup d'instruction sur la page de login if (url.hostname === "prisms.lezinter.net" && url.pathname === "/fr/login") { - log("📘 Injection du popup d'instruction sur la page de login Prisms."); + log("Injection du popup d'instruction sur la page de login Prisms."); showInstructionPopup(details); } // Récupération du token sur la page /balex if (url.hostname === "prisms.lezinter.net" && url.pathname === "/fr/headquarters/balex") { - log("🟢 Page /balex détectée. Tentative de récupération du token."); + log("Page /balex détectée. Tentative de récupération du token."); try { await new Promise(resolve => setTimeout(resolve, 3000)); await browser.tabs.executeScript(details.tabId, { code: ` (function() { - log("🔠Recherche du token..."); + log("Recherche du token..."); const tokenElement = document.getElementById("accessToken") || document.getElementById("accesToken"); if (tokenElement) { const token = tokenElement.innerText.trim(); - log("🔠Token détecté :", token); + log("Token détecté :", token); browser.runtime.sendMessage({ action: "saveToken", token }); } else { - log("⌠Token introuvable."); + log("Token introuvable."); } return null; })(); ` }); } catch (error) { - log("⌠Erreur lors de la récupération du token :", error); + log("Erreur lors de la récupération du token :", error); } } }, { url: [{ hostContains: "prisms.lezinter.net" }] }); -// ───────────────────────────────────────────────────────────────────────────── -// Web Request : Redirection automatique vers /balex -// ───────────────────────────────────────────────────────────────────────────── +/** + * Redirige automatiquement vers /balex. + * @param {Object} details - Détails de la requête. + */ browser.webRequest.onBeforeRequest.addListener( function(details) { if (details.url === "https://prisms.lezinter.net/fr/headquarters/") { - log("🚀 Redirection automatique vers /balex."); + log("Redirection automatique vers /balex."); return { redirectUrl: AUTH_BALEX_URL }; } }, @@ -310,9 +257,10 @@ browser.webRequest.onBeforeRequest.addListener( ["blocking"] ); -// ───────────────────────────────────────────────────────────────────────────── -// Affichage d'un popup d'instruction sur /fr/login -// ───────────────────────────────────────────────────────────────────────────── +/** + * Affiche un popup d'instruction sur /fr/login. + * @param {Object} details - Détails de l'onglet. + */ function showInstructionPopup(details) { browser.tabs.executeScript(details.tabId, { code: ` @@ -362,11 +310,126 @@ function showInstructionPopup(details) { }); } +// ───────────────────────────────────────────────────────────────────────────── +// Gestion des messages reçus +// ───────────────────────────────────────────────────────────────────────────── +/** + * Écouteur de messages reçus dans le script d'arrière-plan + */ +browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => { + log("[Background] Message reçu :", message); + + // Traitement des actions basées sur le message reçu + switch (message.action) { + case "toggleAuth": { + // Vérifie si l'utilisateur est connecté + const isConnected = await isUserConnected(); + if (isConnected) { + await disconnectFromLexicalDB(); + await browser.storage.local.remove("lexiconColors"); + } else { + actuallyOpenLoginPage(); + } + break; + } + case "getDefinitionWiki": { + // Vérifie si le texte sélectionné est valide + if (message.selectedText && message.selectedText.trim() !== "") { + log("Requête Wiktionnaire pour :", message.selectedText); + // Récupère la définition du texte sélectionné depuis le Wiktionnaire + const definition = await window.fetchWiktionaryDefinition(message.selectedText.trim()); + // Envoie la réponse avec la définition récupérée + browser.runtime.sendMessage({ + action: "fetchWiktionaryDefinitionResponse", + selectedText: message.selectedText, + definitions: [{ + source: "Wiktionnaire", + text: definition, + }], + }); + } else { + console.warn("âš ï¸ Texte sélectionné vide. Annulation de la requête."); + } + break; + } + case "checkAuthStatus": { + // Vérifie l'état de connexion de l'utilisateur et envoie la réponse + const connected = await isUserConnected(); + sendResponse(connected); + break; + } + case "authStatusChanged": { + // Log l'état d'authentification mis à jour + log("Mise à jour de l'état d'authentification :", message.isLoggedIn); + break; + } + case "saveToken": { + // Vérifie si un token a été reçu et le sauvegarde + if (message.token) { + await saveToken(message.token); + } else { + console.warn("âš ï¸ Aucune valeur de token reçue."); + } + break; + } + case "toggleLexiconHighlight": { + // Récupère l'onglet actif et applique le script de surlignage + const tabs = await browser.tabs.query({active: true, currentWindow: true}); + if (tabs[0]) { + try { + // Injecte le script de surlignage dans l'onglet actif + await browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ["src/utils/highlighting.js"] + }); + + // Envoie un message pour activer ou désactiver le surlignage + await browser.tabs.sendMessage(tabs[0].id, { + command: message.isActive ? "activate-highlighting" : "deactivate-highlighting", + lexiconId: message.lexiconId + }); + log(`✅ Message de surlignage transmis à l'onglet ${tabs[0].id}`); + } catch (error) { + log("Erreur lors de la gestion du surlignage:", error); + } + } + break; + } + + case "register-highlighting-script": { + // Log l'enregistrement du script de surlignage pour l'onglet + log("Script de surlignage enregistré pour l'onglet", sender.tab.id); + break; + } + + case "toggleExtension": { + // Met à jour l'état de l'extension en fonction du message reçu + const newState = message.isActive; + isExtensionActive = newState; + browser.storage.local.set({ extensionActive: isExtensionActive }); + log("État de l'extension mis à jour :", isExtensionActive); + break; + } + + case "word-added": + log(`[Background] Mot ajouté : '${data.word}' dans '${data.language}' (Lexiques: ${data.lexicons})`); + break; + + default: + break; + } + return true; // Indique que la réponse sera envoyée de manière asynchrone +}); + // ───────────────────────────────────────────────────────────────────────────── // Initialisation du WebWorker // ───────────────────────────────────────────────────────────────────────────── let worker = null; +/** + * Initialise le WebWorker pour le traitement en arrière-plan. + * @returns {void} + */ function initWorker() { if (!worker) { log("[Background] Initialisation du WebWorker..."); @@ -382,40 +445,49 @@ function initWorker() { } } +/** + * Gère les erreurs du WebWorker. + * @param {Error} error - L'erreur. + */ function handleWorkerError(error) { log("Erreur du WebWorker :", error.message); } +/** + * Gère les messages du WebWorker. + * @param {Object} event - L'événement. + */ function handleWorkerMessage(event) { const data = event.data; log("[Background] Message du WebWorker :", data); switch (data.type) { case "pyodide-simplemma": + // Vérifie le statut de Pyodide et Simplemma if (data.status === "success") { log("[Background] Pyodide et Simplemma prêts. Mise à jour de l'état."); + // Met à jour l'état dans le stockage local browser.storage.local.set({ pyodideSimplemmaReady: true }); + // Vérifie et met à jour le suivi des statistiques checkAndUpdateTracking(); } else if (data.status === "error") { - log("[Background] Erreur lors du chargement :", data.message); - } else if (data.status === "already_loaded") { - log("[Background] Pyodide et Simplemma déjà chargés."); + log("[Background] Erreur lors du chargement :", data.message); + } else if (data.status === "already_loaded") { + log("[Background] Pyodide et Simplemma déjà chargés."); } break; case "update-frequencies": log("[Background] Mise à jour des fréquences :", data.frequencies); + // Notifie tous les onglets de la mise à jour des fréquences notifyAllTabs({ command: "update-frequencies", frequencies: data.frequencies }); - // Si un port stats est connecté, vous pouvez lui transmettre également : - // port.postMessage({ command: "update-frequencies", frequencies: data.frequencies }); + // Met à jour les fréquences dans le stockage local browser.storage.local.set({ lemmaFrequencies: data.frequencies }); break; case "threshold-exceeded": log("[Background] Mots dépassant le seuil :", data.wordsAboveThreshold); + // Notifie tous les onglets que le seuil a été dépassé notifyAllTabs({ command: "threshold-exceeded", wordsAboveThreshold: data.wordsAboveThreshold }); break; - case "word-added": - log(`[Background] Mot ajouté : '${data.word}' dans '${data.language}' (Lexiques: ${data.lexicons})`); - break; default: console.warn("[Background] Message non traité du Worker :", data); break; @@ -426,11 +498,11 @@ function handleWorkerMessage(event) { initWorker(); // ───────────────────────────────────────────────────────────────────────────── -// Écoute des messages de la popup et transmission au WebWorker +// Écoute des messages de l'extension et transmission au WebWorker // ───────────────────────────────────────────────────────────────────────────── browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => { log("[Background] Message reçu :", message); - + // Initialise le WebWorker si ce n'est pas déjà fait if (!worker) { initWorker(); } @@ -447,6 +519,7 @@ browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (message.command === "pyodide-simplemma") { log("[Background] Demande d'initialisation de Pyodide et Simplemma..."); + // Envoie un message au WebWorker pour initialiser Pyodide et Simplemma worker.postMessage({ command: "pyodide-simplemma" }); } @@ -564,6 +637,10 @@ browser.runtime.onInstalled.addListener(sendLexiconsToWorker); // ───────────────────────────────────────────────────────────────────────────── // Chargement et sauvegarde des fréquences stockées // ───────────────────────────────────────────────────────────────────────────── +/** + * Charge les fréquences stockées depuis le stockage local. + * @returns {Promise<Object>} Un objet contenant les fréquences stockées. + */ async function loadStoredFrequencies() { const { storedFrequencies } = await browser.storage.local.get("storedFrequencies"); return storedFrequencies || {}; @@ -579,6 +656,11 @@ loadStoredFrequencies().then(frequencies => { // ───────────────────────────────────────────────────────────────────────────── // Statistiques : Vérification et activation/désactivation du tracking // ───────────────────────────────────────────────────────────────────────────── +/** + * Vérifie et met à jour l'état du suivi des statistiques. + * Active ou désactive le suivi en fonction des préférences. + * @returns {Promise<void>} + */ async function checkAndUpdateTracking() { const { isTrackingActive, pyodideSimplemmaReady } = await browser.storage.local.get(["isTrackingActive", "pyodideSimplemmaReady"]); if (isTrackingActive && pyodideSimplemmaReady) { @@ -593,6 +675,10 @@ async function checkAndUpdateTracking() { // ───────────────────────────────────────────────────────────────────────────── // Statistiques : Gestion des onglets // ───────────────────────────────────────────────────────────────────────────── +/** + * Envoie un message à tous les onglets. + * @param {Object} message - Le message à envoyer. + */ async function notifyAllTabs(message) { browser.tabs.query({}).then((tabs) => { tabs.forEach((tab) => { @@ -606,15 +692,22 @@ async function notifyAllTabs(message) { // Statistiques : Écoute des modifications du stockage et mise à jour du tracking // ───────────────────────────────────────────────────────────────────────────── browser.storage.onChanged.addListener(async (changes, area) => { + // Vérifie si les changements concernent le stockage local et les paramètres de suivi if (area === "local" && (changes.isTrackingActive || changes.pyodideSimplemmaReady)) { + // Met à jour l'état du suivi des statistiques checkAndUpdateTracking(); } + + // Vérifie si les changements concernent les préférences d'authentification ou de configuration if (area === "local" && (changes.accessToken || changes.threshold || changes.trackedLanguages || changes.autoAdd)) { log("[Background] Mise à jour des préférences détectée."); - const { accessToken, trackedLanguages, threshold, autoAdd} = await browser.storage.local.get([ + // Récupère les valeurs mises à jour depuis le stockage local + const { accessToken, trackedLanguages, threshold, autoAdd } = await browser.storage.local.get([ "accessToken", "trackedLanguages", "threshold", "autoAdd" ]); + // Vérifie si l'utilisateur est authentifié const isAuthenticated = !!accessToken; + // Envoie un message au WebWorker pour mettre à jour les préférences worker.postMessage({ command: "update-preferences", isAuthenticated, @@ -673,10 +766,68 @@ browser.runtime.onConnect.addListener((port) => { } }); +// ───────────────────────────────────────────────────────────────────────────── +// Stoplists : Chargement et envoi au Worker +// ───────────────────────────────────────────────────────────────────────────── +let stoplistFr = []; + +/** + * Charge la stoplist depuis un fichier et l'envoie au Worker. + * @returns {void} + */ +function loadStoplist() { + fetch(browser.runtime.getURL("stoplist_fr.txt")) + .then(response => response.text()) + .then(text => { + stoplistFr = text.split("\n").map(word => word.trim()); + log("[Background] Stoplist chargée :", stoplistFr); + sendStoplistToWorker(); + }) + .catch(error => log("[Background] Erreur lors du chargement de la stoplist :", error)); +} + +/** + * Envoie la stoplist au Worker. + * @returns {void} + */ +function sendStoplistToWorker() { + log("[Background] Envoi de la stoplist au Worker..."); + worker.postMessage({ command: "update-stoplist", stoplist: stoplistFr }); +} + +browser.runtime.onStartup.addListener(loadStoplist); +browser.runtime.onInstalled.addListener(loadStoplist); // ───────────────────────────────────────────────────────────────────────────── -// (Code commenté concernant l'activation/désactivation de l'analyse) +// Surlignage // ───────────────────────────────────────────────────────────────────────────── -// async function initializeExtensionState() { ... } -// initializeExtensionState(); +/** + * Gestion du surlignage. + * @param {string} command - La commande à exécuter. + * @param {string} lexiconId - L'identifiant du lexique. + * @param {number} tabId - L'identifiant de l'onglet. + * @returns {Promise<boolean>} - True si le surlignage a été effectué, false sinon. + */ +async function handleHighlighting(command, lexiconId, tabId) { + log(`🎯 Gestion du surlignage: ${command} pour le lexique ${lexiconId}`); + + try { + // S'assurer que le script est injecté + await browser.scripting.executeScript({ + target: { tabId: tabId }, + files: ["utils/highlighting.js"] + }); + // Envoyer le message d'activation + const response = await browser.tabs.sendMessage(tabId, { + command: command, + lexiconId: lexiconId + }); + + log("Réponse du content script:", response); + return response; + } catch (error) { + log("Erreur lors de la gestion du surlignage:", error); + return false; + } +} diff --git a/src/context_menu/browser_context_menu.js b/src/context_menu/browser_context_menu.js index cd98a6f297007beac7902932d13bc43505cacf24..2c65412fd3a8e1e12b807e55087eb8ccd15b5105 100644 --- a/src/context_menu/browser_context_menu.js +++ b/src/context_menu/browser_context_menu.js @@ -1,34 +1,37 @@ -log("browser_context_menu.js chargé correctement"); - -let authToken = null; -let selectedLexicons = new Set(); +// ───────────────────────────────────────────────────────────────────────────── +// Variables globales et logs +// ───────────────────────────────────────────────────────────────────────────── +log("browser_context_menu.js chargé."); +let authToken = null; // Token d'authentification +let selectedLexicons = new Set(); // Ensemble des lexiques sélectionnés +// ───────────────────────────────────────────────────────────────────────────── +// Fonctions liées à l'authentification et au menu contextuel +// ───────────────────────────────────────────────────────────────────────────── /** - * Charge le token depuis le stockage local et le stocke dans la variable globale authToken. + * Charge le token depuis le stockage local. */ async function loadAuthToken() { try { const result = await browser.storage.local.get("accessToken"); authToken = result.accessToken; - log("🔑 Token chargé au démarrage :", authToken); + log("Token chargé au démarrage :", authToken); } catch (error) { - log("⌠Erreur lors de la récupération du token :", error); + log("Erreur lors de la récupération du token :", error); } } /** * Crée le menu contextuel en fonction de l'authentification. - * Si l'utilisateur est connecté, on ajoute un item pour la recherche et - * un menu parent pour l'ajout du mot avec des cases à cocher pour chaque lexique - * et un item de confirmation. */ async function createContextMenu() { - await browser.contextMenus.removeAll(); + await browser.contextMenus.removeAll(); // Supprime tous les éléments du menu contextuel const { extensionActive } = await browser.storage.local.get("extensionActive"); log("État de l'extension :", extensionActive); if (extensionActive) { + // Création des éléments du menu contextuel si l'extension est active browser.contextMenus.create({ id: "searchInLexicons", title: "Rechercher dans mes lexiques", @@ -59,6 +62,7 @@ async function createContextMenu() { log("âš ï¸ L'extension est désactivée, aucune option d'analyse ne sera affichée."); } + // Création de l'élément de menu pour la connexion/déconnexion browser.contextMenus.create({ id: "login", title: authToken ? "Se déconnecter de BaLex" : "Se connecter à BaLex", @@ -66,19 +70,21 @@ async function createContextMenu() { }); } - +// Chargement du token et création du menu contextuel loadAuthToken().then(createContextMenu); +// Écoute des messages pour rafraîchir l'interface utilisateur browser.runtime.onMessage.addListener((message) => { if (message.action === "refreshUI" || message.action === "updateUI") { - log("🔄 refreshUI reçu dans browser_context_menu.js"); + log("refreshUI reçu dans browser_context_menu.js"); loadAuthToken().then(createContextMenu); } }); +// Écoute des changements dans le stockage local browser.storage.onChanged.addListener((changes, area) => { if (area === "local" && changes.accessToken) { - log("🔄 Token modifié, actualisation du menu contextuel."); + log("Token modifié, actualisation du menu contextuel."); loadAuthToken().then(createContextMenu); } }); @@ -91,7 +97,7 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => { // Action pour le bouton de connexion/déconnexion if (info.menuItemId === "login") { - log("🔄 Action login/déconnexion demandée."); + log("Action login/déconnexion demandée."); if (authToken) { await disconnectFromLexicalDB(); } else { diff --git a/src/context_menu/custom_context_menu.js b/src/context_menu/custom_context_menu.js index 0c74f175906ad8750c62d8d3d8bcfb04c1343a10..bac66085f8d021d0cbc2b6db11203bae856ec6f6 100644 --- a/src/context_menu/custom_context_menu.js +++ b/src/context_menu/custom_context_menu.js @@ -1,9 +1,18 @@ -log("custom_context_menu.js chargé correctement"); - -// === Variables globales === -const WHITE_BOX_ID = "whiteBox"; +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Variables globales et logs +// ───────────────────────────────────────────────────────────────────────────── +log("custom_context_menu.js chargé."); +const CUSTOM_CONTEXT_MENU = "customContextMenu"; -// Fonction utilitaire pour envoyer une notification via le background +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Fonctions utilitaires +// ───────────────────────────────────────────────────────────────────────────── +/** + * Envoie une notification via le background. + * @param {string} title - Le titre de la notification. + * @param {string} message - Le message de la notification. + * @param {string} iconPath - Le chemin de l'icône de la notification. + */ function sendNotification(title, message, iconPath) { browser.runtime.sendMessage({ action: "showNotification", @@ -13,38 +22,57 @@ function sendNotification(title, message, iconPath) { }); } -// Récupère le token depuis le stockage local et le stocke dans authToken +/** + * Récupère le token depuis le stockage local et le stocke dans authToken. + */ async function loadAuthToken() { try { const result = await browser.storage.local.get("accessToken"); authToken = result.accessToken || null; - log("🔑 Token chargé :", authToken); + log("Token chargé :", authToken); } catch (error) { - log("⌠Erreur lors de la récupération du token :", error); + log("Erreur lors de la récupération du token :", error); authToken = null; } } /** - * Crée le menu contextuel personnalisé (whiteBox) s'il n'existe pas déjà . + * Récupère le texte affiché dans #selectedWord. + * @returns {string} - Le mot sélectionné ou une chaîne vide. */ -function injectWhiteBox() { - let whiteBox = document.getElementById(WHITE_BOX_ID); - if (!whiteBox) { - whiteBox = document.createElement("div"); - whiteBox.id = WHITE_BOX_ID; - whiteBox.style.position = "absolute"; - whiteBox.style.zIndex = "9999"; - whiteBox.style.backgroundColor = "#fff"; - whiteBox.style.border = "1px solid #ccc"; - whiteBox.style.padding = "5px"; - whiteBox.style.borderRadius = "4px"; - whiteBox.style.boxShadow = "0px 2px 10px rgba(0,0,0,0.2)"; +function getSelectedWord() { + const selectedWordElement = document.getElementById("selectedWord"); + return selectedWordElement ? selectedWordElement.textContent.trim() : ""; +} + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Fonctions liées au menu contextuel +// ───────────────────────────────────────────────────────────────────────────── +/** + * Crée le menu contextuel personnalisé (customContextMenu) s'il n'existe pas déjà . + * @returns {HTMLElement} - Le menu contextuel créé ou récupéré. + */ +function injectCustomContextMenu() { + // Récupère le menu contextuel existant par son ID + let customContextMenu = document.getElementById(CUSTOM_CONTEXT_MENU); + + // Si le menu n'existe pas, on le crée + if (!customContextMenu) { + customContextMenu = document.createElement("div"); + customContextMenu.id = CUSTOM_CONTEXT_MENU; + customContextMenu.style.position = "absolute"; + customContextMenu.style.zIndex = "9999"; + customContextMenu.style.backgroundColor = "#fff"; + customContextMenu.style.border = "1px solid #ccc"; + customContextMenu.style.padding = "5px"; + customContextMenu.style.borderRadius = "4px"; + customContextMenu.style.boxShadow = "0px 2px 10px rgba(0,0,0,0.2)"; const addLexiconPath = browser.runtime.getURL("src/assets/icons/ajout_lexique.png"); const getDefinitionPath = browser.runtime.getURL("src/assets/icons/definition.png"); const loginPath = browser.runtime.getURL("src/assets/icons/connexion.png"); + // Construction du HTML du menu contextuel - whiteBox.innerHTML = ` + customContextMenu.innerHTML = ` <p id="selectedWord" style="margin: 0; padding: 0;">Mot sélectionné : Aucun</p> <hr style="border: 0; height: 1px; background-color: #323046; margin: 8px 0;"> <div style="display: flex; flex-wrap: wrap; justify-content: center;"> @@ -62,26 +90,32 @@ function injectWhiteBox() { </div> </div> `; - document.body.appendChild(whiteBox); - setupWhiteBoxActions(); + + // Ajoute le menu contextuel au corps du document + document.body.appendChild(customContextMenu); + + // Configure les actions des boutons du menu contextuel + setupCustomContextMenuActions(); } - whiteBox.addEventListener("mouseup", (e) => { + customContextMenu.addEventListener("mouseup", (e) => { e.stopPropagation(); }); - return whiteBox; + + return customContextMenu; } /** - * Renvoie le whiteBox s'il existe, ou le crée. + * Renvoie le customContextMenu s'il existe, ou le crée. + * @returns {HTMLElement} - Le menu contextuel. */ -function getOrCreateWhiteBox() { - return document.getElementById(WHITE_BOX_ID) || injectWhiteBox(); +function getOrCreateCustomContextMenu() { + return document.getElementById(CUSTOM_CONTEXT_MENU) || injectCustomContextMenu(); } /** * Configure les actions des boutons du menu contextuel. */ -function setupWhiteBoxActions() { +function setupCustomContextMenuActions() { const addLexiconBtn = document.getElementById("addLexiconButton"); const getDefinitionBtn = document.getElementById("getDefinitionButton"); const loginBtn = document.getElementById("loginButton"); @@ -95,7 +129,7 @@ function setupWhiteBoxActions() { e.stopPropagation(); e.preventDefault(); const selectedText = getSelectedWord().trim(); - log("🔠Bouton Ajouter au lexique cliqué avec le mot :", selectedText); + log("Bouton Ajouter au lexique cliqué avec le mot :", selectedText); if (!selectedText) return; if (authToken) { browser.runtime.sendMessage({ action: "openLexiconBlock" }); @@ -126,7 +160,7 @@ function setupWhiteBoxActions() { * Met à jour la visibilité des boutons du menu selon l'authentification. */ function updateMenuVisibility() { - getOrCreateWhiteBox(); + getOrCreateCustomContextMenu(); const addLexiconBtn = document.getElementById("addLexiconButton"); const getDefinitionBtn = document.getElementById("getDefinitionButton"); const loginBtn = document.getElementById("loginButton"); @@ -141,31 +175,25 @@ function updateMenuVisibility() { getDefinitionBtn.style.display = "inline-block"; loginBtn.style.display = "none"; } else { - hideWhiteBox(); + hideCustomContextMenu(); addLexiconBtn.style.display = "none"; getDefinitionBtn.style.display = "inline-block"; loginBtn.style.display = "inline-block"; } } -/** - * Récupère le texte affiché dans #selectedWord. - */ -function getSelectedWord() { - const selectedWordElement = document.getElementById("selectedWord"); - return selectedWordElement ? selectedWordElement.textContent.trim() : ""; -} - /** * Affiche le menu contextuel à la position du clic. + * @param {MouseEvent} event - L'événement de clic. + * @param {string} selectedText - Le texte sélectionné. */ -async function showWhiteBox(event, selectedText) { +async function showCustomContextMenu(event, selectedText) { const { extensionActive } = await browser.storage.local.get("extensionActive") || { extensionActive: false }; if (!extensionActive || !authToken) { - hideWhiteBox(); + hideCustomContextMenu(); return; } - const whiteBox = getOrCreateWhiteBox(); + const customContextMenu = getOrCreateCustomContextMenu(); const selectedWordElement = document.getElementById("selectedWord"); selectedWordElement.textContent = selectedText; @@ -176,87 +204,63 @@ async function showWhiteBox(event, selectedText) { const top = rect.bottom + window.scrollY; const left = rect.right + window.scrollX; - whiteBox.style.left = left + "px"; - whiteBox.style.top = top + "px"; - whiteBox.style.display = "block"; + customContextMenu.style.left = left + "px"; + customContextMenu.style.top = top + "px"; + customContextMenu.style.display = "block"; log("Affichage du menu contextuel avec le mot :", selectedText); updateMenuVisibility(); } -function hideWhiteBox() { - const whiteBox = document.getElementById(WHITE_BOX_ID); - if (whiteBox) { - whiteBox.style.display = "none"; +/** + * Masque le menu contextuel personnalisé. + */ +function hideCustomContextMenu() { + const customContextMenu = document.getElementById(CUSTOM_CONTEXT_MENU); + if (customContextMenu) { + customContextMenu.style.display = "none"; } } -// Écoute globale pour la sélection de texte -document.addEventListener("mouseup", (event) => { - if (event.target.closest("#whiteBox")) return; - const selectedText = window.getSelection().toString().trim(); - if (selectedText) { - log("Texte sélectionné :", selectedText); - getOrCreateWhiteBox(); - showWhiteBox(event, selectedText); - browser.runtime.sendMessage({ - action: "mot_selectionne", - selectedText, - }); - } else { - hideWhiteBox(); - } -}); - -// Écoute des messages entrants -browser.runtime.onMessage.addListener((message) => { - if (message.action === "refreshUI") { - log("🔄 Mise à jour du menu contextuel personnalisé."); - loadAuthToken().then(updateMenuVisibility); - } -}); - -// Initialisation au démarrage -loadAuthToken().then(() => { - getOrCreateWhiteBox(); - updateMenuVisibility(); -}); -browser.storage.onChanged.addListener((changes) => { - if (changes.accessToken) { - log("🔄 Token modifié dans le stockage, mise à jour du menu contextuel."); - loadAuthToken().then(updateMenuVisibility); - } -}); - // ───────────────────────────────────────────────────────────────────────────── -// Fonctions d'API pour l'ajout d'un mot via le sélecteur +// Fonctions pour l'ajout d'un mot via le sélecteur // ───────────────────────────────────────────────────────────────────────────── /** * Affiche le sélecteur pour choisir le lexique dans lequel ajouter le mot. + * @param {MouseEvent} event - L'événement de clic. + * @param {string} selectedText - Le texte sélectionné. */ async function showPicker(event, selectedText) { + // Récupère le sélecteur de lexique existant par son ID let picker = document.getElementById("lexiconPicker"); + + // Si le sélecteur n'existe pas, on le crée if (!picker) { picker = document.createElement("div"); picker.id = "lexiconPicker"; picker.addEventListener("mouseup", (e) => e.stopPropagation()); document.body.appendChild(picker); } - picker.innerHTML = ""; - const selectedLexicons = new Set(); + + picker.innerHTML = ""; + const selectedLexicons = new Set(); // Ensemble pour stocker les lexiques sélectionnés try { + // Récupère les lexiques disponibles en utilisant le token d'authentification const lexicons = await getLexicons(authToken); log("Lexicons récupérés :", lexicons); - const lexiconDescriptions = {}; + const lexiconDescriptions = {}; // Objet pour stocker les descriptions des lexiques + // Vérifie si des lexiques ont été récupérés if (!Array.isArray(lexicons) || lexicons.length === 0) { picker.innerHTML = "<p style='color:#333;'>Aucun lexique trouvé.</p>"; } else { + // Parcourt chaque lexique récupéré for (const lex of lexicons) { - const id = lex.id; - let name = ""; + const id = lex.id; // Récupère l'ID du lexique + let name = ""; // Initialise le nom du lexique + // Définit le nom en fonction de la catégorie du lexique if (lex.category === "User") { name = DEBUG ? `Lexique personnel : ${lex.user?.pseudo || "Inconnu"} (${lex.id})` @@ -267,29 +271,32 @@ async function showPicker(event, selectedText) { : `Lexique de groupe : ${lex.group?.name || "Inconnu"}`; } if (lex.language) { - name += ` [${lex.language}]`; + name += ` [${lex.language}]`; // Ajoute la langue si disponible } - lexiconDescriptions[id] = name; - const color = await getColorForLexicon(id); - const circleIcon = await createColorCircle(color, 28); + lexiconDescriptions[id] = name; // Stocke la description du lexique + const color = await getColorForLexicon(id); // Récupère la couleur du lexique + const circleIcon = await createColorCircle(color, 28); + // Crée un conteneur pour l'icône du lexique const iconContainer = document.createElement("div"); iconContainer.className = "lexicon-option"; iconContainer.dataset.lexiconId = id; iconContainer.title = name; + iconContainer.addEventListener("click", () => { if (selectedLexicons.has(id)) { - selectedLexicons.delete(id); - iconContainer.classList.remove("selected"); + selectedLexicons.delete(id); // Supprime l'ID si déjà sélectionné + iconContainer.classList.remove("selected"); // Retire la classe sélectionnée } else { - selectedLexicons.add(id); - iconContainer.classList.add("selected"); + selectedLexicons.add(id); // Ajoute l'ID si non sélectionné + iconContainer.classList.add("selected"); // Ajoute la classe sélectionnée } }); - iconContainer.appendChild(circleIcon); - picker.appendChild(iconContainer); + iconContainer.appendChild(circleIcon); // Ajoute l'icône au conteneur + picker.appendChild(iconContainer); // Ajoute le conteneur au sélecteur } + // Crée le bouton de confirmation pour ajouter le mot const confirmButton = document.createElement("button"); confirmButton.className = "confirmButton"; confirmButton.textContent = "Ajouter le mot"; @@ -301,32 +308,33 @@ async function showPicker(event, selectedText) { log(`🔠Vérification si le mot "${selectedText}" existe déjà dans les lexiques sélectionnés...`); let definitions = []; try { - definitions = await fetchLexiconDefinitions(selectedText); + definitions = await fetchLexiconDefinitions(selectedText); // Récupère les définitions du mot } catch (error) { log("Erreur lors de la récupération des définitions :", error); } - const existingLexiconIds = new Set(); + const existingLexiconIds = new Set(); // Ensemble pour stocker les IDs des lexiques existants if (Array.isArray(definitions)) { for (const def of definitions) { if (selectedLexicons.has(def.lexiconId)) { - existingLexiconIds.add(def.lexiconId); + existingLexiconIds.add(def.lexiconId); // Ajoute l'ID si le mot existe déjà } } } + // Alerte si le mot existe déjà dans les lexiques sélectionnés if (existingLexiconIds.size > 0) { alert(`Le mot "${selectedText}" existe déjà dans les lexiques suivants : ${Array.from(existingLexiconIds).map(id => lexiconDescriptions[id]).join(", ")}`); } - const lexiconsToAdd = [...selectedLexicons].filter(id => !existingLexiconIds.has(id)); + const lexiconsToAdd = [...selectedLexicons].filter(id => !existingLexiconIds.has(id)); // Filtre les lexiques à ajouter if (lexiconsToAdd.length === 0) { - return; + return; // Sort si aucun lexique à ajouter } try { log(`📡 Ajout du mot "${selectedText}" dans les lexiques :`, lexiconsToAdd); - const result = await AddWord(authToken, selectedText, lexiconsToAdd, false); + const result = await AddWord(authToken, selectedText, lexiconsToAdd, false); // Ajoute le mot aux lexiques log("Réponse API :", result); - await new Promise(resolve => setTimeout(resolve, 300)); - browser.runtime.sendMessage({ action: "refreshUI" }); - const successMsg = `✅ Mot ajouté avec succès dans : ${lexiconsToAdd.map(id => lexiconDescriptions[id]).join(", ")}`; + await new Promise(resolve => setTimeout(resolve, 300)); // Attente pour la réponse + browser.runtime.sendMessage({ action: "refreshUI" }); // Rafraîchit l'interface utilisateur + const successMsg = `✅ Mot ajouté avec succès dans : ${lexiconsToAdd.map(id => lexiconDescriptions[id]).join(", ")}`; // Message de succès picker.innerHTML = `<p style="color: green;">${successMsg}</p>`; setTimeout(() => picker.style.display = "none", 2000); browser.runtime.sendMessage({ @@ -334,7 +342,7 @@ async function showPicker(event, selectedText) { lexicons: successMsg }); } catch (error) { - log("⌠Erreur lors de l'ajout du mot :", error); + log("Erreur lors de l'ajout du mot :", error); const errorMsg = `⌠Erreur lors de l'ajout du mot : ${error.message}`; picker.innerHTML = `<p style="color: red;">${errorMsg}</p>`; setTimeout(() => picker.style.display = "none", 3000); @@ -344,24 +352,28 @@ async function showPicker(event, selectedText) { }); } }); - picker.appendChild(confirmButton); + picker.appendChild(confirmButton); // Ajoute le bouton de confirmation au sélecteur } - const lexiconCount = Array.isArray(lexicons) ? lexicons.length : 0; - const baseWidthPerLexicon = 40; - const extraPadding = 20; - const calculatedWidth = lexiconCount * baseWidthPerLexicon + extraPadding; - picker.style.width = calculatedWidth + "px"; + // Calcule et définit la largeur et la position du sélecteur + const lexiconCount = Array.isArray(lexicons) ? lexicons.length : 0; + const baseWidthPerLexicon = 40; + const extraPadding = 20; + const calculatedWidth = lexiconCount * baseWidthPerLexicon + extraPadding; + picker.style.width = calculatedWidth + "px"; picker.style.left = event.pageX + "px"; picker.style.top = event.pageY + "px"; picker.style.display = "flex"; } catch (error) { - log("⌠Erreur lors de la récupération des lexiques :", error); + log("Erreur lors de la récupération des lexiques :", error); picker.innerHTML = "<p style='color:#333;'>Erreur lors du chargement des lexiques.</p>"; picker.style.display = "block"; } } +/** + * Masque le sélecteur de lexique. + */ function hideLexiconPicker() { const picker = document.getElementById("lexiconPicker"); if (picker) { @@ -369,21 +381,73 @@ function hideLexiconPicker() { } } +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Écouteurs d'événements +// ───────────────────────────────────────────────────────────────────────────── +// Écoute globale pour la sélection de texte document.addEventListener("mouseup", (event) => { - const whiteBox = document.getElementById(WHITE_BOX_ID); - const picker = document.getElementById("lexiconPicker"); + // Vérifie si le clic est à l'intérieur du menu contextuel, si oui, ne fait rien + if (event.target.closest("#customContextMenu")) return; + + // Récupère le texte sélectionné + const selectedText = window.getSelection().toString().trim(); + if (selectedText) { + log("Texte sélectionné :", selectedText); // Log le texte sélectionné + getOrCreateCustomContextMenu(); // Récupère ou crée le menu contextuel + showCustomContextMenu(event, selectedText); // Affiche le menu contextuel avec le texte sélectionné + // Envoie un message au runtime avec le texte sélectionné + browser.runtime.sendMessage({ + action: "mot_selectionne", + selectedText, + }); + } else { + hideCustomContextMenu(); + } +}); + +// Écoute des messages entrants +browser.runtime.onMessage.addListener((message) => { + if (message.action === "refreshUI") { + log("Mise à jour du menu contextuel personnalisé."); + loadAuthToken().then(updateMenuVisibility); + } +}); + +// Initialisation au démarrage +loadAuthToken().then(() => { + getOrCreateCustomContextMenu(); // Récupère ou crée le menu contextuel + updateMenuVisibility(); // Met à jour la visibilité des boutons +}); + +// Écoute des changements dans le stockage +browser.storage.onChanged.addListener((changes) => { + // Vérifie si le token d'accès a changé + if (changes.accessToken) { + log("Token modifié dans le stockage, mise à jour du menu contextuel."); + loadAuthToken().then(updateMenuVisibility); // Recharge le token et met à jour la visibilité + } +}); + +// Écouteur pour masquer le menu contextuel et le sélecteur +document.addEventListener("mouseup", (event) => { + const customContextMenu = document.getElementById(CUSTOM_CONTEXT_MENU); // Récupère le menu contextuel + const picker = document.getElementById("lexiconPicker"); // Récupère le sélecteur de lexique - if (whiteBox && !whiteBox.contains(event.target)) { - hideWhiteBox(); + // Masque le menu contextuel si le clic est en dehors de celui-ci + if (customContextMenu && !customContextMenu.contains(event.target)) { + hideCustomContextMenu(); } + // Masque le sélecteur si le clic est en dehors de celui-ci if (picker && !picker.contains(event.target)) { hideLexiconPicker(); } + // Récupère le texte sélectionné const selectedText = window.getSelection().toString().trim(); if (selectedText) { log("Texte sélectionné :", selectedText); - showWhiteBox(event, selectedText); + showCustomContextMenu(event, selectedText); + // Envoie un message au runtime avec le texte sélectionné browser.runtime.sendMessage({ action: "mot_selectionne", selectedText, diff --git a/src/css/custom_context_menu.css b/src/css/custom_context_menu.css new file mode 100644 index 0000000000000000000000000000000000000000..66c89329490d1300b780f57fdec0fb096ddd99d9 --- /dev/null +++ b/src/css/custom_context_menu.css @@ -0,0 +1,155 @@ +/* Import de la police Luciole */ +@font-face { + font-family: 'Luciole'; + src: url('../fonts/Luciole-Regular/Luciole-Regular.woff2') format('woff2'), + url('../fonts/Luciole-Regular/Luciole-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +/* Conteneur principal du menu contextuel */ +#customContextMenu { + position: absolute; + display: none; + min-width: 50px; + max-width: 300px; + background-color: white; + color: #323046; + border: 2px solid #323046; + border-radius: 10px; + padding: 10px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + font-family: Luciole; + z-index: 10000; +} + +/* Mot sélectionné */ +#customContextMenu #selectedWord { + margin: 0; + margin-bottom: 8px; + font-size: 14px; + line-height: 1.3; + color: #323046; + font-weight: bold; + text-align: center; +} + +/* Icônes */ +#customContextMenu .icon-container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + margin: 0; + padding: 0; +} +#customContextMenu .icon-container:hover { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 6px; +} +#customContextMenu .icon { + width: 40px; + height: 40px; + transition: transform 0.2s ease; + margin : 0 auto; + display: block; +} +#customContextMenu .icon:hover { + transform: scale(1.15); +} + +/* Messages d'information (tooltips) */ +#customContextMenu .tooltip { + visibility: hidden; + background-color: #333; + color: #fff; + text-align: center; + padding: 6px; + border-radius: 5px; + position: absolute; + bottom: -34px; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 12px; + opacity: 0; + transition: opacity 0.2s ease, visibility 0.2s ease; + z-index: 1000; +} +#customContextMenu .icon-container:hover .tooltip { + visibility: visible; + opacity: 1; +} + +/* Style du sélectionneur de lexiques */ +#lexiconPicker { + position: absolute; + z-index: 10000; + background-color: rgba(255, 255, 255, 0.98); + border: 1px solid #ddd; + padding: 4px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: auto; + font-family: Luciole; + font-size: 10px; + display: flex; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center; + justify-content: center; + gap: 2px; +} +#lexiconPicker p { + font-size: 12px; + font-weight: bold; + text-align: center; +} + +/* Style pour les icônes de lexique */ +#lexiconPicker .lexicon-option { + cursor: pointer; + display: inline-flex !important; + flex-direction: row !important; + align-items: center; + justify-content: center; + border: 2px solid transparent; + border-radius: 50%; + width: 40px; + height: 40px; + transition: border 0.2s ease; + flex: 0 0 auto; +} +/* Effet au survol pour les icônes */ +#lexiconPicker .lexicon-option:hover { + border: 2px solid #6e76c7; +} +/* Indiquer qu'une icône est sélectionnée */ +#lexiconPicker .lexicon-option.selected { + border: 2px solid #323046; +} +#lexiconPicker .color-circle { + width: 28px; + height: 28px; + border-radius: 50%; + display: inline-block; + border: 1px solid black; +} + +/* Style pour le bouton de confirmation */ +#lexiconPicker button.confirmButton { + font-style: italic; + font-size: 10px; + padding: 6px 10px; + cursor: pointer; + border: none; + align-items: center; + text-align: center; + border-radius: 4px; + background-color: #323046; + color: white; + flex-basis: 100%; + margin-top: 8px; +} diff --git a/src/css/plugin.css b/src/css/plugin.css new file mode 100644 index 0000000000000000000000000000000000000000..6d8a5bd7eabc3e9efa15602cf54db1a5e29de149 --- /dev/null +++ b/src/css/plugin.css @@ -0,0 +1,372 @@ +/* Import de la police Luciole */ +@font-face { + font-family: 'Luciole'; + src: url('../fonts/Luciole-Regular/Luciole-Regular.woff2') format('woff2'), + url('../fonts/Luciole-Regular/Luciole-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +/* Styles généraux */ +body { + font-family: Luciole; + font-size: medium; + margin: 0; + padding: 8px; + background-color: #525877; + color: #323046; + border-radius: 10px; + width: 200px; +} +.hidden { + display: none; +} +button { + font-family: Luciole; + width: 100%; + padding: 12px; + margin-bottom: 8px; + font-size: 14px; + font-weight: bold; + border: none; + cursor: pointer; + border-radius: 6px; + transition: background 0.3s, transform 0.2s; + background-color: #a08e9f; + color: white; +} +button:hover { + background-color: #dddedd; + color: #8d5c70; +} + +/* En-tête de l'extension */ +#extension-name { + font-family: Luciole; + text-align: center; + font-size: 23px; + font-weight: bold; + padding: 5px; + color: #8d5c70; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: white; +} + +/* Bouton de connexion */ +#auth-button { + width: auto; + display: inline-flex; + padding: 6px 12px; + font-size: 16px; + font-family: Luciole; + background: none; + border: none; + color: white; + align-items: center; + gap: 6px; + border-radius: 20px; + cursor: pointer; + transition: background 0.3s; + text-align: center; +} +#auth-button:hover { + background: rgba(255,255,255,0.2); +} +#auth-button svg { + width: 18px; + height: 18px; + fill: white; + transition: transform 0.3s ease-in-out; +} +#auth-button:hover svg { + transform: scale(1.1); +} +#auth-section { + display: flex; + justify-content: center; + margin-bottom: 10px; +} + +/* Options du menu de l'extension */ +.option-container { + background: #444; + padding: 8px; + border-radius: 10px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); +} +.option-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + padding: 12px; + border-radius: 6px; + background-color: #444; + color: white; + transition: transform 0.2s, box-shadow 0.2s; +} +.option-row:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} +.option-row label { + font-weight: lighter; + flex: 1; + margin: 0; + font-size: 13px; + align-items: center; +} +.option-row input[type="checkbox"], +.option-row input[type="number"] { + appearance: none; + width: 20px; + height: 20px; + border: 2px solid #8d5c70; + border-radius: 50%; + background-color: #fff; + cursor: pointer; + transition: background-color 0.3s, border-color 0.3s; +} +.option-row input[type="checkbox"]:checked { + background-color: #8d5c70; + border-color: #8d5c70; +} + +/* Bouton pour ouvrir les statistiques */ +#open-stats { + padding: 6px; + font-weight: lighter; + width: auto; + display: block; + margin: 0 auto; + margin-bottom: 10px; + background-color: #525877; + color: white; + border: 2px solid #8d5c70; + border-radius: 8px; +} + +/* Bloc d'options d'ajout automatique */ +.option-row.auto-add-row { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + margin-bottom: 0; + background: transparent; + border-bottom: 1px solid #555; +} +.option-row.auto-add-row span { + font-size: 14px; + font-weight: lighter; +} +.option-row.stopwords { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 13px; + font-weight: lighter; +} + +/* Bouton pour afficher/masquer le bloc d'options d'ajout automatique et la sélection d'options*/ +.toggle-switch { + position: absolute; + display: inline-block; + width: 30px; + height: 16px; + right: 0; + left: auto; +} +.toggle-switch .slider:before { + position: absolute; + content: ""; + width: 12px; + height: 12px; + left: 2px; + bottom: 2px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; +} +input:checked + .slider { + background-color: #8d5c70; +} +input:checked + .slider:before { + transform: translateX(14px); +} + +/* Conteneur pour le seuil d'ajout automatique */ +.threshold-container input[type="number"] { + width: 45px; + height: 45px; + line-height: 50px; + text-align: center; + border-radius: 50%; + border: 2px solid #8d5c70; + font-size: 13px; + box-sizing: border-box; + background: #fff; + color: #333; +} + +/* Sélection des langues */ +.language-selection { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.lang-option { + padding: 4px 10px; + border: 2px solid #8d5c70; + border-radius: 16px; + background: rgb(152, 152, 152); + color: #8d5c70; + cursor: pointer; + font-size: 0.85rem; + transition: background 0.3s, color 0.3s, transform 0.2s; +} +.lang-option:hover { + background: #8d5c70; + color: white; + transform: scale(1.05); +} +.lang-option.selected { + background: #8d5c70; + color: white; + border-color: #8d5c70; +} + +/* Bouton pour enregistrer les options */ +#save-options { + border: none; + background: #8d5c70; + border-radius: 6px; + color: white; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s, transform 0.2s; +} +#save-options:hover { + background: #ccc; + color: #8d5c70; + transform: translateY(-2px); +} + +/* Messages d'information (tooltips) */ +.tooltip-container { + position: relative; + display: inline-block; + pointer-events: auto !important; +} +.tooltip { + all: unset; + display: block; + box-sizing: border-box; + position: absolute; + left: 50%; + transform: translateX(-50%); + color: #fff !important; + font-size: 12px !important; + font-weight: lighter !important; + padding: 6px 10px; + border-radius: 5px; + white-space: normal; + overflow-wrap: break-word; + width: 200px; + text-align: center; + visibility: hidden; + transition: visibility 0.3s ease-in-out, transform 0.3s ease-in-out; + pointer-events: none; + z-index: 1000; + line-height: normal; +} +.tooltip-container .tooltip { + bottom: 120%; + transform: translateX(-50%); + background-color: rgba(0,0,0,0.9) !important; + visibility: hidden; + pointer-events: auto !important; +} +.tooltip-container:hover .tooltip { + visibility: visible !important; + transform: translateX(-50%) translateY(-5px); + pointer-events: auto !important; +} +#auth-button .tooltip { + top: 120%; + bottom: auto; +} +#auth-button.tooltip-container:hover .tooltip { + visibility: visible !important; + transform: translateX(-50%) translateY(5px); +} +/* Permettre l'interaction avec les tooltips même sur les boutons désactivés */ +button:disabled .tooltip { + pointer-events: auto !important; +} + +.tooltip-langues-suivies { + left: 50% !important; + transform: translateX(-28%) !important; +} + +/* Notifications et erreurs */ +#extension-notification { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.85); + color: white; + padding: 15px; + border-radius: 8px; + text-align: center; + width: 80%; + max-width: 250px; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + transition: visibility 0.3s ease; +} +#extension-notification.hidden { + visibility: hidden; +} + +#close-notification { + margin-top: 10px; + padding: 5px 10px; + background-color: #8d5c70; + color: white; + border: none; + cursor: pointer; + border-radius: 5px; + font-weight: bold; +} +#close-notification:hover { + background-color: #dddedd; + color: #8d5c70; +} +#error-message { + font-size: 13px; + font-style: italic; + text-align: center; + color: white; +} diff --git a/src/css/sidebar.css b/src/css/sidebar.css new file mode 100644 index 0000000000000000000000000000000000000000..76e6dc47dca1b3ee394a2140d59132ae7a59df7e --- /dev/null +++ b/src/css/sidebar.css @@ -0,0 +1,399 @@ +/* Import de la police Luciole */ +@font-face { + font-family: 'Luciole'; + src: url('../fonts/Luciole-Regular/Luciole-Regular.woff2') format('woff2'), + url('../fonts/Luciole-Regular/Luciole-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +/* Style global */ +body { + font-family: Luciole; + font-size: medium; + margin: 0; + padding: 10px; + background-color: #525877; + color: #323046; +} + +/* Conteneurs principaux */ +#menu, #etat, #definitionContainer { + padding: 10px; + margin-bottom: 10px; + border-radius: 10px; + background-color: #a08e9f; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + overflow: visible; +} + +/* En-têtes et contenu des blocs */ +.block-header { + position: relative; + text-align: center; + margin-top: 2px; +} +.block-header h3 { + display: inline-block; + margin-top: 5px; + margin-bottom: 0; +} +.block-content { + padding-top: 2px; +} +.hidden { + display: none; +} + +/* Boutons */ +button { + font-family: Luciole; + width: 100%; + margin-top: 5px; + padding: 10px; + border: none; + background-color: #8d5c70; + color: #fbfcfc; + font-weight: bold; + cursor: pointer; + text-align: center; + border-radius: 5px; +} +button:hover { + background-color: #dddedd; + color: #8d5c70; +} + +/* Bouton de connexion */ +#auth-button { + width: auto; + display: inline-flex; + padding: 6px 12px; + font-size: 16px; + font-family: Luciole; + background: none; + border: none; + color: white; + align-items: center; + gap: 6px; + border-radius: 20px; + cursor: pointer; + transition: background 0.3s; +} +#auth-button:hover { + background: rgba(255,255,255,0.2); +} +#auth-button svg { + width: 18px; + height: 18px; + fill: white; + transition: transform 0.3s ease-in-out; +} +#auth-button:hover svg { + transform: scale(1.1); +} +#auth-section { + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +/* Boutons de bascule (toggle) */ +.toggle-btn { + position: absolute; + right: 8px; + background: none; + margin: 0; + border: none; + color: #fff; + font-size: 15px; + padding: 3px 5px; + cursor: pointer; + width: auto; + display: inline-block; +} + +/* Mot sélectionné */ +#motSelectionne { + font-style: italic; + text-align: center; + margin-top: 5px; +} + +/* Style des lexiques */ +.lexique-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px; + margin-bottom: 5px; + border-radius: 5px; + background-color: #dcdde1; + position: relative; +} +.lexique-item:hover { + background-color: #c4c7ce; +} +.lexique-label { + font-weight: bold; + color: #323046; + flex-grow: 1; + font-size: 12px; + text-align: center; +} + +/* Icône du lexique */ +.lexique-icon { + width: 25px; + height: 25px; + border-radius: 50%; + background-color: #ccc; + margin-right: 10px; + flex-shrink: 0; +} + +/* Messages d'information (tooltips) */ +.tooltip { + all: unset; + display: block; + box-sizing: border-box; + position: absolute; + bottom: 120%; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0,0,0,0.75); + color: #fff; + font-size: 12px !important; + font-weight: lighter !important; + padding: 6px 10px; + border-radius: 5px; + white-space: normal; + overflow-wrap: break-word; + width: 180px; + text-align: center; + opacity: 0; + transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out; + pointer-events: none; + z-index: 10; + line-height: normal; +} +.tooltip-container { + position: relative; + display: inline-block; + cursor: pointer; + overflow: visible; +} +.tooltip-container:hover .tooltip { + opacity: 1; + transform: translateX(-50%) translateY(-5px); +} +.tooltip-container.left .tooltip { + left: 0; + transform: translateX(0) translateY(-5px); +} +.tooltip-container.right .tooltip { + right: 0; + left: auto; + transform: translateX(0) translateY(-5px); +} +/* Tooltip pour le bouton de surlignage */ +button.lexique-highlight-toggle .tooltip { + all: unset; + display: block; + box-sizing: border-box; + position: absolute; + bottom: 120%; + left: 50%; + transform: translateX(-50%) translateY(-5px); + background-color: rgba(0, 0, 0, 0.75); + color: #fff; + font-size: 14px; + font-weight: lighter; + padding: 6px 10px; + border-radius: 5px; + white-space: normal; + overflow-wrap: break-word; + width: 180px; + text-align: center; + opacity: 0; + transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out; + pointer-events: none; + z-index: 10; + line-height: normal; +} +button.lexique-highlight-toggle:hover .tooltip { + opacity: 1; + transform: translateX(-50%) translateY(-5px); +} + +/* Section Lexique */ +.lexicon-section { + margin-bottom: 10px; +} +.lexicon-header { + font-weight: bold; + cursor: pointer; + padding: 5px; + background-color: #8d5c70; + border-radius: 5px; + text-align: center; +} +.lexicon-header:hover { + background-color: #dddedd; + color: #8d5c70; +} +.lexicon-content { + margin-top: 5px; +} +.lexicon-option { + margin-right: -10px; +} +#mesLexiquesList { + display: inline; + padding: 0; + align-items: center; +} +#mesLexiquesContainer h4 { + margin-bottom: 5px; +} + +/* Cases à cocher pour les lexiques */ +.lexique-checkbox { + appearance: none; + width: 20px; + height: 20px; + border: 2px solid #8d5c70; + border-radius: 5px; + background-color: #fff; + transition: background 0.3s ease, border-color 0.3s ease; + cursor: pointer; + position: relative; +} +.lexique-checkbox:hover { + border-color: #6a3e50; +} +.lexique-checkbox:checked { + background-color: #8d5c70; + border-color: #8d5c70; +} +.lexique-checkbox:checked::after { + content: '✔'; + font-size: 16px; + color: white; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Surlignage */ +.lexique-highlight-toggle { + background: none; + border: none; + cursor: pointer; + padding: 2px; + transition: transform 0.2s ease-in-out; + width: 15%; + position: relative; +} +.feutre-icon { + width: 20px; + height: 20px; + filter: brightness(0) saturate(100%) invert(40%) sepia(0%) saturate(0%) hue-rotate(0deg); + transition: filter 0.3s ease-in-out; +} +/* Icone active */ +.lexique-highlight-toggle.active .feutre-icon, +.lexique-highlight-toggle[data-active="true"] .feutre-icon { + filter: brightness(0) saturate(100%) invert(83%) sepia(89%) saturate(588%) hue-rotate(360deg); +} +.lexicon-highlight { + position: relative; + display: inline-block; + padding-bottom: 4px; + border-bottom: 1px dashed #666; + transition: background-color 0.3s; + background-color: rgba(255, 255, 0, 0.15); + +} +/* Bandes de couleurs pour le surlignage */ +.color-bands { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 3px; + display: flex; +} +.color-bands div { + flex: 1; + height: 100%; +} +#highlighting-options p { + margin: 5px 0; + font-size: small; + color: #333; +} + +/* Section Définitions */ +#definitionContainer { + background-color: #444; + padding: 10px; + border-radius: 10px; + color: white; +} +#definitionsList { + list-style: none; + padding: 0; +} +#definitionsList li { + margin-bottom: 10px; +} +.definition-source { + font-weight: bold; + color: #ffa500; +} +#noDefinitionsContainer { + display: block !important; + color: red !important; + font-weight: bold; +} + +/* Modal de définition */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.5); + display: none; + align-items: center; + justify-content: center; + z-index: 9999; +} +.modal-content { + background: white; + color: #8d5c70; + padding: 1rem; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + border-radius: 8px; +} +.close-button { + float: right; + cursor: pointer; + font-weight: bold; + color: #666; +} +.close-button:hover { + color: #000; +} + +/* Message si utilisateur déconnecté/analyse désactivée */ +#messageContainer { + display: none; + text-align: center; + color: #323046; +} \ No newline at end of file diff --git a/src/options/options.html b/src/options/options.html deleted file mode 100644 index 8b2bfa963d5f5369b8b67464fd9ac95a506d4253..0000000000000000000000000000000000000000 --- a/src/options/options.html +++ /dev/null @@ -1,38 +0,0 @@ -<!-- <!DOCTYPE html> -<html lang="fr"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Paramètres ff2BaLex</title> - <script src="../utils/logger.js"></script> - <style> - body { - font-family: Luciole; - padding: 20px; - background-color: #323046; - color: white; - } - button { - width: 100%; - padding: 10px; - margin-bottom: 10px; - font-size: 15px; - color: #94608a; - font-weight: bold; - border: none; - cursor: pointer; - } - button:hover { - background-color: #94608a; - color: white; - } - </style> -</head> -<body> - <h3>Paramètres BaLex</h3> - <button id="connectBtn">Se connecter</button> - <button id="toggleExtensionBtn">Activer/Désactiver l'extension</button> - <button id="toggleStatsBtn">Activer/Désactiver les statistiques</button> - <script src="options.js"></script> -</body> -</html> --> diff --git a/src/plugin/plugin.html b/src/plugin/plugin.html new file mode 100644 index 0000000000000000000000000000000000000000..98cb1c18abfeeac6e7fa50a33c7fb66b1c1dc9a3 --- /dev/null +++ b/src/plugin/plugin.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html lang="fr"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Extension BaLex</title> + <script src="../utils/logger.js"></script> + <link rel="stylesheet" href="../css/plugin.css"> +</head> +<body> + <div id="extension-name">Extension BaLex</div> + + <!-- Section de connexion --> + <div id="auth-section"> + <button id="auth-button"> + <span id="auth-icon"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path d="M10 2a1 1 0 0 1 1 1v2h2V3a1 1 0 0 1 2 0v2h2a2 2 0 0 1 2 2v3h-2V7h-2v3a1 1 0 1 1-2 0V7h-2v3a1 1 0 1 1-2 0V7H6v3H4V7a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1Z"/> + </svg> + </span> + <span id="auth-text">Se connecter</span> + </button> + </div> + + <!-- Section des options de l'extension --> + <button id="toggleExtensionBtn">Activer/Désactiver</button> + <button id="toggleStatsBtn">Statistiques</button> + <button id="open-stats">Afficher les statistiques</button> + <!-- Indicateur de chargement de Pyodide --> + <div id="pyodide-loading"></div> + + <!-- Section des options d'ajout automatique --> + <div id="stats-options" class="option-container"> + <div id="auto-add-container" class="option-row auto-add-row"> + <span>Ajout automatique</span> + <label class="toggle-switch"> + <input type="checkbox" id="auto-add"> + <span class="slider"></span> + </label> + </div> + <div id="auto-add-options" class="hidden"> + <div class="option-row stopwords"> + <span>Inclure mots outils</span> + <label class="toggle-switch"> + <input type="checkbox" id="include-stopwords" /> + <span class="slider"></span> + </label> + </div> + <div class="option-row threshold-container"> + <label for="threshold">Seuil d'ajout d'un mot</label> + <input type="number" id="threshold" value="10" min="1" /> + </div> + <div class="option-row"> + <label>Langues suivies</label> + <div id="language-selection" class="language-selection"> + <p id="loading-languages" style="color: gray;">Chargement...</p> + </div> + </div> + <!-- Message d'erreur si aucune langue sélectionnée --> + <div id="error-message" class="hidden"> + <p>Veuillez sélectionner une ou plusieurs langue(s).</p> + </div> + <button id="save-options" class="hidden">Valider</button> + </div> + </div> + + <!-- Notification de l'extension --> + <div id="extension-notification" class="hidden"> + <p id="notification-text"></p> + <button id="close-notification">OK</button> + </div> + + <script src="../utils/api.js"></script> + <script src="plugin.js"></script> +</body> +</html> diff --git a/src/plugin/plugin.js b/src/plugin/plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..f95ae3890bded642a9276209a4c33485b8ae581b --- /dev/null +++ b/src/plugin/plugin.js @@ -0,0 +1,479 @@ +log("plugin.js chargé."); +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Fonctions utilitaires +// ───────────────────────────────────────────────────────────────────────────── +/** + * Récupère le token d'accès depuis le stockage local. + * @returns {Promise<string>} Le token d'accès. + */ +async function getAccessToken() { + const { accessToken } = await browser.storage.local.get("accessToken"); + return accessToken; +} + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Gestion de la connexion +// ───────────────────────────────────────────────────────────────────────────── +/** + * Met à jour le bouton de connexion en fonction de l'état de connexion. + */ +async function updateConnectionButton() { + // Récupération du token d'accès + const accessToken = await getAccessToken(); + // Sélection du bouton de connexion + const button = document.getElementById("auth-button"); + + // Vérification si le bouton existe + if (!button) { + log("Le bouton de connexion n'a pas été trouvé."); + return; + } + + // Mise à jour du bouton en fonction de l'état de connexion + if (accessToken) { + button.textContent = "Se déconnecter"; + button.style.position = "relative"; + button.className = "tooltip-container"; + const tooltip = document.createElement("span"); + tooltip.className = "tooltip"; + tooltip.textContent = "En vous déconnectant, vous perdrez l'accès à vos lexiques personnels, ainsi qu'aux fonctionnalités d'ajout automatique et de statistiques d'utilisation."; + button.appendChild(tooltip); + } else { + button.textContent = "Se connecter"; + button.style.position = "relative"; + button.className = "tooltip-container"; + const tooltip = document.createElement("span"); + tooltip.className = "tooltip"; + tooltip.textContent = "En vous connectant, vous pourrez accéder à vos lexiques personnels, ainsi qu'aux fonctionnalités d'ajout automatique et de statistiques d'utilisation."; + button.appendChild(tooltip); + } + + // Gestion du clic sur le bouton + button.onclick = async () => { + await browser.runtime.sendMessage({ action: "toggleAuth" }); // Envoi d'un message pour changer l'état d'authentification + await updateConnectionButton(); // Mise à jour du bouton après le changement d'état + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Gestion de la sélection des langues +// ───────────────────────────────────────────────────────────────────────────── +/** + * Met à jour la sélection des langues. + */ +async function updateLanguageSelection() { + const languageSelection = document.getElementById("language-selection"); + languageSelection.innerHTML = "<p id='loading-languages' style='color: gray;'>Chargement...</p>"; + + const accessToken = await getAccessToken(); + // Vérification si l'utilisateur est connecté + if (!accessToken) { + languageSelection.innerHTML = "<p style='color: red;'>Veuillez vous connecter.</p>"; + return; + } + + // Récupération des lexiques et langues de l'utilisateur + const lexicons = await getLexicons(accessToken); + const userLanguages = [...new Set(lexicons.map(lex => lex.language))]; + + // Récupération des langues suivies depuis le stockage local + const { trackedLanguages } = await browser.storage.local.get("trackedLanguages") || { trackedLanguages: [] }; + + languageSelection.innerHTML = ""; + if (userLanguages.length === 0) { + languageSelection.innerHTML = "<p style='color: red;'>Aucun lexique personnel trouvé.</p>"; // Message d'erreur si aucun lexique + return; + } + + // Création des boutons pour chaque langue + userLanguages.forEach(lang => { + const langButton = document.createElement("div"); + langButton.classList.add("lang-option"); + langButton.textContent = lang.toUpperCase(); + langButton.dataset.value = lang; + // Vérification si la langue est suivie + if (trackedLanguages && trackedLanguages.includes(lang)) { + langButton.classList.add("selected"); + } + // Gestion du clic sur le bouton de langue + langButton.addEventListener("click", () => { + langButton.classList.toggle("selected"); + }); + languageSelection.appendChild(langButton); + }); + + log("Sélection des langues mise à jour avec :", userLanguages); +} + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Gestion principale de l'extension +// ───────────────────────────────────────────────────────────────────────────── +/** + * Met à jour l'extension. + */ +async function updateExtension() { + // Récupération de tous les états nécessaires + const accessToken = await getAccessToken(); + const { + extensionActive, + isTrackingActive, + autoAdd, + threshold, + pyodideSimplemmaReady + } = await browser.storage.local.get([ + "extensionActive", + "isTrackingActive", + "autoAdd", + "threshold", + "pyodideSimplemmaReady" + ]) || { + extensionActive: false, + isTrackingActive: false, + autoAdd: false, + threshold: 10, + pyodideSimplemmaReady: false + }; + + // Vérification si l'utilisateur est connecté + const isLoggedIn = !!accessToken; + + // Mise à jour du bouton d'activation de l'extension + const toggleExtensionBtn = document.getElementById("toggleExtensionBtn"); + if (toggleExtensionBtn) { + // Mise à jour du texte du bouton en fonction de l'état de l'extension + toggleExtensionBtn.textContent = extensionActive ? "Désactiver l'analyse" : "Activer l'analyse"; + toggleExtensionBtn.style.pointerEvents = isLoggedIn ? "auto" : "none"; + toggleExtensionBtn.disabled = !isLoggedIn; + toggleExtensionBtn.style.position = "relative"; + toggleExtensionBtn.className = "tooltip-container"; + + // Suppression de l'ancien tooltip s'il existe + const existingTooltipExt = toggleExtensionBtn.querySelector('.tooltip'); + if (existingTooltipExt) { + existingTooltipExt.remove(); + } + // Création d'un nouveau tooltip + const tooltipExt = document.createElement("span"); + tooltipExt.className = "tooltip"; + tooltipExt.style.opacity = "1 !important"; + if (!isLoggedIn) { + tooltipExt.textContent = "Connectez-vous pour activer l'analyse"; + tooltipExt.style.display = "block"; + } else if (!extensionActive) { + tooltipExt.textContent = "Activer les fonctionnalités de l'extension : affichage des mots et des définitions de vos lexiques, ajout de mots, etc."; + tooltipExt.style.display = "block"; + } else { + tooltipExt.style.display = "none"; + } + toggleExtensionBtn.appendChild(tooltipExt); + + toggleExtensionBtn.onclick = handleToggleExtension; + } + + // Mise à jour des options de statistiques + const statsOptions = document.getElementById("stats-options"); + const toggleStatsBtn = document.getElementById("toggleStatsBtn"); + const openStats = document.getElementById("open-stats"); + + // Affichage ou masquage des options de statistiques + if (statsOptions) { + statsOptions.style.display = (isLoggedIn && extensionActive) ? "block" : "none"; + } + + // Mise à jour du bouton des statistiques + if (toggleStatsBtn) { + const isEnabled = isLoggedIn && extensionActive; + toggleStatsBtn.textContent = isEnabled && isTrackingActive ? "Désactiver les statistiques" : "Activer les statistiques"; + toggleStatsBtn.style.pointerEvents = isEnabled ? "auto" : "none"; + toggleStatsBtn.disabled = !isEnabled; + toggleStatsBtn.style.position = "relative"; + toggleStatsBtn.className = "tooltip-container"; + + // Suppression de l'ancien tooltip s'il existe + const existingTooltipStats = toggleStatsBtn.querySelector('.tooltip'); + if (existingTooltipStats) { existingTooltipStats.remove(); } + + // Création d'un nouveau tooltip pour les statistiques + const tooltipStats = document.createElement("span"); + tooltipStats.className = "tooltip"; + tooltipStats.style.opacity = "1 !important"; + if (!isLoggedIn) { + tooltipStats.textContent = "Connectez-vous pour accéder aux statistiques"; + tooltipStats.style.display = "block"; + } else if (!extensionActive) { + tooltipStats.textContent = "Veuillez activer l'analyse pour utiliser les statistiques"; + tooltipStats.style.display = "block"; + } else { + tooltipStats.style.display = "none"; + } + toggleStatsBtn.appendChild(tooltipStats); + } + + // Affichage ou masquage du bouton pour ouvrir les statistiques + if (openStats) { + openStats.style.display = (isLoggedIn && extensionActive && isTrackingActive) ? "block" : "none"; + } + + // Mise à jour des options d'ajout automatique + const autoAddContainer = document.getElementById("auto-add")?.parentElement; + const autoAddCheckbox = document.getElementById("auto-add"); + const autoAddOptions = document.getElementById("auto-add-options"); + const saveOptionsBtn = document.getElementById("save-options"); + + // Affichage ou masquage du conteneur d'ajout automatique + if (autoAddContainer) { + autoAddContainer.style.display = (isLoggedIn && extensionActive) ? "block" : "none"; + } + + // Mise à jour de l'état de la case à cocher d'ajout automatique + if (autoAddCheckbox && isLoggedIn) { + autoAddCheckbox.checked = autoAdd; + } + + // Affichage ou masquage des options d'ajout automatique + if (autoAddOptions) { + autoAddOptions.classList.toggle("hidden", !autoAdd); + } + + // Affichage ou masquage du bouton de sauvegarde des options + if (saveOptionsBtn) { + saveOptionsBtn.classList.toggle("hidden", !autoAdd); + } + + // Mise à jour du message de chargement Pyodide + const statusContainer = document.getElementById('pyodide-loading'); + if (statusContainer) { + if (!isLoggedIn) { + statusContainer.innerHTML = ""; + } else if (!pyodideSimplemmaReady && extensionActive && isTrackingActive) { + statusContainer.innerHTML = "<p style='color: black; text-align: center; font-size: 11px;'>Chargement de l'extension en cours, veuillez patienter...</p>"; + } else if (pyodideSimplemmaReady && extensionActive && isTrackingActive) { + statusContainer.innerHTML = "<p style='color: black; text-align: center; font-size: 11px;'>C'est prêt !</p>"; + setTimeout(() => { + statusContainer.innerHTML = ""; + }, 2000); + } else { + statusContainer.innerHTML = ""; + } + } + + // Mise à jour de la sélection des langues + await updateLanguageSelection(); + + log("Interface mise à jour complètement", { + isLoggedIn, + extensionActive, + isTrackingActive, + autoAdd, + pyodideSimplemmaReady + }); +} + +/** + * Gère le clic sur le bouton d'activation de l'extension (analyse). + */ +async function handleToggleExtension() { + const accessToken = await getAccessToken(); + if (!accessToken) return; + await proceedToggleExtension(); + await updateConnectionButton(); +} + +/** + * Effectue le changement d'état de l'extension (analyse). + */ +async function proceedToggleExtension() { + // Récupération de l'état actuel de l'extension et du suivi + const { extensionActive, isTrackingActive } = await browser.storage.local.get({ + extensionActive: false, // Valeur par défaut si non trouvée + isTrackingActive: false // Valeur par défaut si non trouvée + }); + log("État actuel de extensionActive avant changement :", extensionActive); + + const newState = !extensionActive; + // Mise à jour de l'état de l'extension dans le stockage local + await browser.storage.local.set({ extensionActive: newState }); + log("Nouvel état de extensionActive :", newState); + + // Envoi d'un message pour mettre à jour l'interface utilisateur + browser.runtime.sendMessage({ action: "updateUI", extensionActive: newState }); + + // Si l'extension est désactivée + if (!newState) { + // Désactivation du suivi + await browser.storage.local.set({ isTrackingActive: false }); + // Ouverture de la page des statistiques si le suivi était actif + if (isTrackingActive) window.open("stats.html", "_blank"); + // Envoi d'un message pour fermer les blocs de la barre latérale + browser.runtime.sendMessage({ action: "closeSidebarBlocks" }); + } + + // Envoi d'un message pour changer l'état de l'extension + browser.runtime.sendMessage({ action: "toggleExtension", isActive: newState }); + // Mise à jour de l'extension après le changement d'état + await updateExtension(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Écouteurs d'événements +// ───────────────────────────────────────────────────────────────────────────── +/** + * Met à jour le bouton de connexion et l'extension lorsque le DOM est chargé. + */ +document.addEventListener("DOMContentLoaded", async () => { + await updateConnectionButton(); + await updateExtension(); +}); + +/** + * Gère le clic sur le bouton des statistiques. + */ +document.getElementById("toggleStatsBtn")?.addEventListener("click", async () => { + const accessToken = await getAccessToken(); + if (!accessToken) return; + + // Récupération de l'état actuel du suivi + const current = await browser.storage.local.get("isTrackingActive"); + const newState = !current.isTrackingActive; + await browser.storage.local.set({ isTrackingActive: newState }); + + // Si le suivi est désactivé, désactiver également l'ajout automatique + if (!newState) { + await browser.storage.local.set({ autoAdd: false }); + } + + // Envoi d'un message pour changer l'état des statistiques + browser.runtime.sendMessage({ command: "toggle-stats", isActive: newState }); + // Si le suivi est activé, envoyer un message pour pyodide-simplemma + if (newState) { + browser.runtime.sendMessage({ command: "pyodide-simplemma" }); + } + + // Mise à jour de l'affichage des lexiques + if (isUpdatingLexicons) return; + isUpdatingLexicons = true; + await updateLexiconsDisplay(); + isUpdatingLexicons = false; + await updateExtension(); +}); + +/** + * Gère le changement de l'ajout automatique. + */ +document.getElementById("auto-add")?.addEventListener("change", async () => { + const isChecked = document.getElementById("auto-add").checked; + document.getElementById("auto-add-options").classList.toggle("hidden", !isChecked); + document.getElementById("save-options").classList.toggle("hidden", !isChecked); + + // Si l'ajout automatique est désactivé, mettre à jour l'état + if (!isChecked) { + await browser.storage.local.set({ autoAdd: false }); + await updateExtension(); + } +}); + +/** + * Sauvegarde des options. + */ +document.getElementById("save-options")?.addEventListener("click", async () => { + const autoAdd = document.getElementById("auto-add").checked; + const threshold = parseInt(document.getElementById("threshold").value, 10); + const selectedLanguages = Array.from(document.querySelectorAll("#language-selection .lang-option.selected")) + .map(option => option.dataset.value); + + const errorMessage = document.getElementById("error-message"); + // Vérification si l'ajout automatique est activé sans langues sélectionnées + if (autoAdd && selectedLanguages.length === 0) { + errorMessage?.classList.remove("hidden"); // Affichage du message d'erreur + return; + } + errorMessage?.classList.add("hidden"); + + // Sauvegarde des options dans le stockage local + await browser.storage.local.set({ + autoAdd, + threshold, + trackedLanguages: selectedLanguages + }); + + await updateExtension(); + log("Options sauvegardées et interface mise à jour"); +}); + +/** + * Ouvre la page des statistiques. + */ +document.getElementById("open-stats")?.addEventListener("click", () => { + window.open("stats.html", "_blank"); +}); + +/** + * Affiche le message d'information sur les langues suivies. + */ +document.addEventListener("DOMContentLoaded", () => { + const optionRows = document.querySelectorAll('.option-row label'); + optionRows.forEach(label => { + if (label.textContent.trim() === "Langues suivies") { + label.classList.add("tooltip-container"); + + const tooltip = document.createElement("span"); + tooltip.className = "tooltip tooltip-langues-suivies"; + tooltip.textContent = "Choisissez une ou plusieurs langues parmi celles de vos lexiques personnels"; + label.appendChild(tooltip); + } + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Gestion des messages +// ───────────────────────────────────────────────────────────────────────────── +browser.runtime.onMessage.addListener(async (message) => { + log("Message reçu dans plugin.js :", message); + if (message.action === "updateUI") { + await updateExtension(); + } else if (message.action === "notify") { + alert(message.message); + } +}); + +browser.storage.onChanged.addListener((changes, area) => { + if (area === "local") { + updateExtension(); + } +}); + +/** + * Affiche une notification. + */ +function showNotification(message) { + const notificationBox = document.getElementById("extension-notification"); + const notificationText = document.getElementById("notification-text"); + const closeButton = document.getElementById("close-notification"); + + if (notificationBox && notificationText && closeButton) { + notificationText.textContent = message; + notificationBox.classList.remove("hidden"); + closeButton.addEventListener("click", () => { + notificationBox.classList.add("hidden"); + }, { once: true }); + } else { + log("Impossible d'afficher la notification : élément manquant."); + } +} + +/** + * Gère les messages reçus du worker. + */ +function handleWorkerMessage(event) { + const data = event.data; + log("[Background] Message du WebWorker :", data); + + if (data.type === "process-text" && data.status === "error") { + browser.runtime.sendMessage({ + action: "notify", + message: data.message + }); + return; + } +} + diff --git a/src/sidebar/sidebar.html b/src/sidebar/sidebar.html index 99ef99e15b5663b39e7fb6895196e675dfe416e6..fce4745e7dfc2f650699a92c3bf1825bb17fd33e 100644 --- a/src/sidebar/sidebar.html +++ b/src/sidebar/sidebar.html @@ -9,409 +9,8 @@ <script src="../utils/api.js" defer></script> <script src="../utils/definitions.js" defer></script> <script src="sidebar.js" defer></script> - <style> - @font-face { - font-family: 'Luciole'; - src: url('../fonts/Luciole-Regular/Luciole-Regular.woff2') format('woff2'), - url('../fonts/Luciole-Regular/Luciole-Regular.woff') format('woff'); - font-weight: normal; - font-style: normal; - } - /* Style global */ - body { - font-family: Luciole; - font-size: medium; - margin: 0; - padding: 10px; - background-color: #525877; - color: #323046; - } + <link rel="stylesheet" href="../css/sidebar.css"> - /* Conteneurs principaux */ - #menu, #etat, #definitionContainer { - padding: 10px; - margin-bottom: 10px; - border-radius: 10px; - background-color: #a08e9f; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - overflow: visible; - } - - /* En-têtes de blocs */ - .block-header { - position: relative; - text-align: center; - margin-top: 2px; - } - .block-header h3 { - display: inline-block; - margin-top: 5px; - margin-bottom: 0; - } - - /* Bouton de connexion */ - #auth-button { - width: auto; - display: inline-flex; - padding: 6px 12px; - font-size: 16px; - font-family: Luciole; - background: none; - border: none; - color: white; - align-items: center; - gap: 6px; - border-radius: 20px; - cursor: pointer; - transition: background 0.3s; - } - #auth-button:hover { - background: rgba(255,255,255,0.2); - } - #auth-button svg { - width: 18px; - height: 18px; - fill: white; - transition: transform 0.3s ease-in-out; - } - #auth-button:hover svg { - transform: scale(1.1); - } - #auth-section { - display: flex; - justify-content: flex-end; - margin-bottom: 10px; - } - - /* Boutons de bascule (toggle) */ - .toggle-btn { - position: absolute; - right: 8px; - background: none; - margin: 0; - border: none; - color: #fff; - font-size: 15px; - padding: 3px 5px; - cursor: pointer; - width: auto; - display: inline-block; - } - - /* Contenu des blocs */ - .block-content { - padding-top: 2px; - } - .hidden { - display: none; - } - - /* Boutons standards */ - button { - font-family: Luciole; - width: 100%; - margin-top: 5px; - padding: 10px; - border: none; - background-color: #8d5c70; - color: #fbfcfc; - font-weight: bold; - cursor: pointer; - text-align: center; - border-radius: 5px; - } - button:hover { - background-color: #dddedd; - color: #8d5c70; - } - - /* Mot sélectionné */ - #motSelectionne { - font-style: italic; - text-align: center; - margin-top: 5px; - } - - /* Style des lexiques */ - .lexique-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px; - margin-bottom: 5px; - border-radius: 5px; - background-color: #dcdde1; - position: relative; - } - .lexique-item:hover { - background-color: #c4c7ce; - } - .lexique-label { - font-weight: bold; - color: #323046; - flex-grow: 1; - font-size: 12px; - text-align: center; - } - - /* Icône du lexique */ - .lexique-icon { - width: 25px; - height: 25px; - border-radius: 50%; - background-color: #ccc; - margin-right: 10px; - flex-shrink: 0; - } - - /* Conteneur pour tooltip (pour checkbox et surlignage) */ - .tooltip-container { - position: relative; - display: inline-block; - cursor: pointer; - overflow: visible; - } - - /* Style pour tous les tooltips */ - .tooltip { - all: unset; - display: block; - box-sizing: border-box; - position: absolute; - bottom: 120%; - left: 50%; - transform: translateX(-50%); - background-color: rgba(0,0,0,0.75); - color: #fff; - font-size: 12px !important; - font-weight: lighter !important; - padding: 6px 10px; - border-radius: 5px; - white-space: normal; - overflow-wrap: break-word; - width: 180px; - text-align: center; - opacity: 0; - transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out; - pointer-events: none; - z-index: 10; - line-height: normal; - } - - .tooltip-container:hover .tooltip { - opacity: 1; - transform: translateX(-50%) translateY(-5px); - } - .tooltip-container.left .tooltip { - left: 0; - transform: translateX(0) translateY(-5px); - } - .tooltip-container.right .tooltip { - right: 0; - left: auto; - transform: translateX(0) translateY(-5px); - } - - /* Cases à cocher personnalisées */ - .lexique-checkbox { - appearance: none; - width: 20px; - height: 20px; - border: 2px solid #8d5c70; - border-radius: 5px; - background-color: #fff; - transition: background 0.3s ease, border-color 0.3s ease; - cursor: pointer; - position: relative; - } - .lexique-checkbox:hover { - border-color: #6a3e50; - } - .lexique-checkbox:checked { - background-color: #8d5c70; - border-color: #8d5c70; - } - .lexique-checkbox:checked::after { - content: '✔'; - font-size: 16px; - color: white; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - /* Bouton de surlignage */ - .lexique-highlight-toggle { - background: none; - border: none; - cursor: pointer; - padding: 2px; - transition: transform 0.2s ease-in-out; - width: 15%; - position: relative; - } - - .feutre-icon { - width: 20px; - height: 20px; - filter: brightness(0) saturate(100%) invert(40%) sepia(0%) saturate(0%) hue-rotate(0deg); - transition: filter 0.3s ease-in-out; - } - - /* Ajout de la classe active */ - .lexique-highlight-toggle.active .feutre-icon, - .lexique-highlight-toggle[data-active="true"] .feutre-icon { - filter: brightness(0) saturate(100%) invert(83%) sepia(89%) saturate(588%) hue-rotate(360deg); - } - - button.lexique-highlight-toggle .tooltip { - all: unset; - display: block; - box-sizing: border-box; - position: absolute; - bottom: 120%; - left: 50%; - transform: translateX(-50%) translateY(-5px); - background-color: rgba(0, 0, 0, 0.75); - color: #fff; - font-size: 14px; - font-weight: lighter; - padding: 6px 10px; - border-radius: 5px; - white-space: normal; - overflow-wrap: break-word; - width: 180px; - text-align: center; - opacity: 0; - transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out; - pointer-events: none; - z-index: 10; - line-height: normal; - } - - button.lexique-highlight-toggle:hover .tooltip { - opacity: 1; - transform: translateX(-50%) translateY(-5px); - } - - .lexicon-highlight { - position: relative; /* Pour positionner le conteneur de bandes en absolu */ - display: inline-block; /* Pour que le span prenne en compte les dimensions */ - padding-bottom: 4px; /* Laisser de l'espace pour les bandes */ - border-bottom: 1px dashed #666; /* Vous pouvez conserver votre bordure si besoin */ - transition: background-color 0.3s; - background-color: rgba(255, 255, 0, 0.15); - - } - - .color-bands { - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 3px; /* Ajustez la hauteur des bandes */ - display: flex; /* Pour répartir équitablement les bandes */ - } - - .color-bands div { - flex: 1; /* Chaque bande occupe une part égale */ - height: 100%; - } - - - .lexicon-section { - margin-bottom: 10px; - } - .lexicon-header { - font-weight: bold; - cursor: pointer; - padding: 5px; - background-color: #8d5c70; - border-radius: 5px; - text-align: center; - } - .lexicon-header:hover { - background-color: #dddedd; - color: #8d5c70; - } - .lexicon-content { - margin-top: 5px; - } - .lexicon-option { - margin-right: -10px; - } - #mesLexiquesList { - display: inline; - padding: 0; - align-items: center; - } - #mesLexiquesContainer h4 { - margin-bottom: 5px; - } - #definitionContainer { - background-color: #444; - padding: 10px; - border-radius: 10px; - color: white; - } - #definitionsList { - list-style: none; - padding: 0; - } - #definitionsList li { - margin-bottom: 10px; - } - .definition-source { - font-weight: bold; - color: #ffa500; - } - .modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0,0,0,0.5); - display: none; - align-items: center; - justify-content: center; - z-index: 9999; - } - .modal-content { - background: white; - color: #8d5c70; - padding: 1rem; - max-width: 600px; - max-height: 80vh; - overflow-y: auto; - border-radius: 8px; - } - .close-button { - float: right; - cursor: pointer; - font-weight: bold; - color: #666; - } - .close-button:hover { - color: #000; - } - #highlighting-options p { - margin: 5px 0; - font-size: small; - color: #333; - } - #noDefinitionsContainer { - display: block !important; - color: red !important; - font-weight: bold; - } - #messageContainer { - display: none; - text-align: center; - color: #323046; - } - </style> </head> <body> <!-- Bouton de connexion --> @@ -430,10 +29,10 @@ <div id="menu"> <div class="block-header"> <h3>Lexiques</h3> - <button class="toggle-btn">+</button> + <button class="toggle-btn">+</button> <!-- Bouton pour afficher/masquer le bloc --> </div> <div id="menuContent" class="block-content hidden"> - <div id="lexiques">Chargement...</div> + <div id="lexiques">Chargement...</div> </div> </div> @@ -443,8 +42,8 @@ <h3>Mot sélectionné</h3> <button class="toggle-btn"></button> </div> - <div id="etatContent" class="block-content hidden"> - <p id="motSelectionne">Aucun mot sélectionné</p> + <div id="etatContent" class="block-content hidden"> + <p id="motSelectionne">Aucun mot sélectionné</p> <p id="lexiconResult"></p> <div id="add-to-lexiques" style="display: none;"> <button id="add-word-button">Ajouter le mot sélectionné</button> @@ -485,7 +84,7 @@ <ul id="wiktionnaireList"></ul> </div> - <!-- Fenêtre modale cachée --> + <!-- Fenêtre modale cachée pour afficher les détails--> <div id="modalOverlay" class="modal-overlay"> <div class="modal-content"> <span id="closeModalBtn" class="close-button">X</span> diff --git a/src/sidebar/sidebar.js b/src/sidebar/sidebar.js index fd756701445b7068fdbd658510171cec9aab04c6..cbf981ba3acaa5fa527f68195ed64b22cb6d1d94 100644 --- a/src/sidebar/sidebar.js +++ b/src/sidebar/sidebar.js @@ -1,20 +1,18 @@ -log("✅ sidebar.js chargé !"); -log( - "🌠Vérification API browser :", - typeof browser !== "undefined" ? "✅ Disponible" : "⌠Non disponible" -); - - // ───────────────────────────────────────────────────────────────────────────── -// â–Œ Variables globales +// â–Œ Variables globales et logs // ───────────────────────────────────────────────────────────────────────────── -let authToken = window.authToken; -window.authToken = authToken; -let isUpdatingLexicons = false; +log("sidebar.js chargé."); +let authToken = window.authToken; // Token d'authentification de l'utilisateur +window.authToken = authToken; // Stockage du token dans l'objet window +let isUpdatingLexicons = false; // Indicateur pour savoir si les lexiques sont en cours de mise à jour // ───────────────────────────────────────────────────────────────────────────── -// â–Œ Fonctions liées au token +// â–Œ Fonctions liées à la connexion et à l'activation de l'analyse // ───────────────────────────────────────────────────────────────────────────── +/** + * Récupère le token d'authentification depuis le stockage local. + * @returns {Promise<string|null>} Le token d'authentification ou null si non trouvé. + */ async function getAuthTokenFromStorage() { try { const { accessToken } = await browser.storage.local.get("accessToken"); @@ -26,24 +24,55 @@ async function getAuthTokenFromStorage() { } log("âš ï¸ Aucun token trouvé dans le stockage local."); } catch (error) { - log("⌠Erreur lors de la récupération du token :", error); + log("Erreur lors de la récupération du token :", error); } return null; } +/** + * Gère le clic sur le bouton d'authentification. + * Envoie un message pour basculer l'état d'authentification et actualise l'interface. + */ +async function handleAuthButtonClick() { + await browser.runtime.sendMessage({ action: "toggleAuth" }); + await refreshSidebarState(); + const messageContainer = document.getElementById("messageContainer"); + if (messageContainer) { + messageContainer.style.display = "block"; + messageContainer.innerHTML = "Veuillez vous connecter pour utiliser l'extension."; + } +} + +/** + * Vérifie l'état d'activation de l'extension. + * @returns {Promise<boolean>} L'état d'activation de l'extension. + */ +async function checkAnalysisStatus() { + const { extensionActive } = await browser.storage.local.get("extensionActive"); + return extensionActive; +} + // ───────────────────────────────────────────────────────────────────────────── // â–Œ Mise à jour de l'interface utilisateur (UI) // ───────────────────────────────────────────────────────────────────────────── +/** + * Met à jour le texte du bouton d'authentification en fonction de l'état de connexion. + * @param {boolean} isLoggedIn - Indique si l'utilisateur est connecté. + */ function updateAuthButton(isLoggedIn) { const authButton = document.getElementById("auth-button"); if (authButton) { authButton.textContent = isLoggedIn ? "Se déconnecter" : "Se connecter"; - log("🔄 Bouton d'authentification mis à jour :", authButton.textContent); + log("Bouton d'authentification mis à jour :", authButton.textContent); } else { console.warn("âš ï¸ Bouton d'authentification (#auth-button) introuvable."); } } +/** + * Bascule la visibilité des éléments en fonction de l'état de connexion. + * @param {boolean} isLoggedIn - Indique si l'utilisateur est connecté. + */ function toggleElementsVisibility(isLoggedIn) { const elementsToShowOrHide = [ { id: "add-to-lexiques", shouldShow: isLoggedIn }, @@ -62,6 +91,10 @@ function toggleElementsVisibility(isLoggedIn) { }); } +/** + * Affichage ou non le message de surlignage en fonction de l'état de connexion. + * @param {boolean} isLoggedIn - Indique si l'utilisateur est connecté. + */ function toggleHighlightMessage(isLoggedIn) { const highlightNote = document.getElementById("highlight-note"); if (highlightNote) { @@ -73,27 +106,33 @@ function toggleHighlightMessage(isLoggedIn) { * Met à jour l'état global de la barre latérale (bouton d'authentification, etc.) */ async function refreshSidebarState() { - log("🔄 Début de l'actualisation de la barre latérale..."); + log("Début de l'actualisation de la barre latérale..."); + + // Récupère le token d'authentification et détermine si l'utilisateur est connecté const token = await getAuthTokenFromStorage(); const isLoggedIn = !!token; + // Met à jour l'état du bouton d'authentification et la visibilité des éléments updateAuthButton(isLoggedIn); toggleElementsVisibility(isLoggedIn); toggleHighlightMessage(isLoggedIn); + // Vérifie si l'analyse est activée const isAnalysisEnabled = await checkAnalysisStatus(); if (isLoggedIn) { + // Si l'utilisateur est connecté, gère l'affichage des blocs et récupère les lexiques hideBlocks(!isAnalysisEnabled); + // Met à jour les couleurs des lexiques si ce n'est pas déjà fait if (!window.lexiconColorsUpdated) { await updateLexiconColors(authToken); window.lexiconColorsUpdated = true; } await fetchLexicons(); } else { - // Si l'utilisateur n'est pas connecté, on ferme tous les blocs - hideBlocks(true); // Masquer tous les blocs + // Si l'utilisateur n'est pas connecté, masque les blocs et affiche un message + hideBlocks(true); const lexiquesContainer = document.getElementById("lexiques"); if (lexiquesContainer) { @@ -107,7 +146,7 @@ async function refreshSidebarState() { } } - // Afficher un message d'activation de l'analyse selon le statut de connexion + // Affiche un message d'activation de l'analyse selon le statut de connexion const messageContainer = document.getElementById("messageContainer"); if (!isLoggedIn) { if (messageContainer) { @@ -125,9 +164,13 @@ async function refreshSidebarState() { } } - log("✅ Barre latérale actualisée. Utilisateur connecté :", isLoggedIn); + log("Barre latérale actualisée. Utilisateur connecté :", isLoggedIn); } + +// ───────────────────────────────────────────────────────────────────────────── +// â–Œ Gestion des blocs (Affichage) +// ───────────────────────────────────────────────────────────────────────────── /** * Bascule l'affichage d'un bloc. * @param {string} blockId - L'ID du conteneur à basculer. @@ -170,6 +213,11 @@ function openBlock(blockId, btn) { } } +/** + * Ferme un bloc s'il est ouvert et met à jour le bouton de bascule. + * @param {string} blockId - L'ID du conteneur à fermer. + * @param {HTMLElement} [btn] - (Optionnel) Le bouton de bascule à mettre à jour. + */ function closeBlock(blockId, btn) { const block = document.getElementById(blockId); if (block && !block.classList.contains("hidden")) { @@ -189,43 +237,54 @@ function closeBlock(blockId, btn) { } } +/** + * Masque ou affiche les blocs en fonction de l'état spécifié. + * @param {boolean} shouldHide - Indique si les blocs doivent être masqués. + */ function hideBlocks(shouldHide) { const blockIds = ["menu", "etat", "definitionContainer"]; blockIds.forEach(blockId => { const block = document.getElementById(blockId); if (block) { if (shouldHide) { - block.classList.add("hidden"); // Masquer le bloc + block.classList.add("hidden"); } else { - block.classList.remove("hidden"); // Afficher le bloc + block.classList.remove("hidden"); } } }); } // ───────────────────────────────────────────────────────────────────────────── -// â–Œ Gestion des lexiques (Affichage) +// â–Œ Gestion de l'affichage des lexiques et du surlignage // ───────────────────────────────────────────────────────────────────────────── - +/** + * Récupère les lexiques depuis l'API. + */ async function fetchLexicons() { try { - log("🔄 Début de la récupération des lexiques..."); - log("🔑 Token utilisé :", authToken); + log("Début de la récupération des lexiques..."); + log("Token utilisé :", authToken); + // Vérifie si le token d'authentification est disponible if (!authToken) { throw new Error("âš ï¸ Aucun token disponible. Veuillez vous connecter."); } + // Récupère les lexiques depuis l'API const lexicons = await getLexicons(authToken); - log("📚 Réponse brute de l'API :", lexicons); + log("Réponse brute de l'API :", lexicons); + // Vérifie si la réponse est un tableau et s'il contient des lexiques if (!Array.isArray(lexicons) || lexicons.length === 0) { throw new Error("âš ï¸ Aucun lexique trouvé."); } + // Vide la carte des lexiques existants lexiconMap.clear(); lexicons.forEach((lex) => { let lexiconName = ""; + // Crée le nom du lexique en fonction de sa catégorie if (lex.category === "User") { lexiconName = DEBUG ? `Lexique personnel : ${lex.user?.pseudo || "Inconnu"} (${lex.id})` @@ -240,9 +299,11 @@ async function fetchLexicons() { } else { lexiconName = DEBUG ? `Lexique : ${lex.id}` : "Lexique" ; } + // Ajoute le lexique à la carte avec son ID comme clé lexiconMap.set(lex.id, lexiconName); }); + // Affiche les lexiques avec des checkboxes await displayLexiconsWithCheckbox(lexicons.map((lex) => ({ lexiconName: lex.category === "User" @@ -256,10 +317,12 @@ async function fetchLexicons() { active: lex.active || false, }))); - // Restaurer l'état des boutons après l'affichage + + // Restaure l'état des boutons de surlignage await restoreHighlightingState(); } catch (error) { - log("⌠Erreur lors du chargement des lexiques :", error.message); + // Gère les erreurs lors du chargement des lexiques + log("Erreur lors du chargement des lexiques :", error.message); const lexiquesContainer = document.getElementById("lexiques"); if (lexiquesContainer) { lexiquesContainer.textContent = error.message || "Erreur lors du chargement des lexiques."; @@ -273,32 +336,43 @@ async function fetchLexicons() { */ async function displayLexiconsWithCheckbox(lexicons) { const lexiquesContainer = document.getElementById("lexiques"); + // Vérifie si le conteneur des lexiques existe if (!lexiquesContainer) { console.warn("âš ï¸ Ã‰lément #lexiques introuvable."); return; } + + // Vide le conteneur avant d'afficher les nouveaux lexiques lexiquesContainer.innerHTML = ""; + + // Vérifie si la liste des lexiques est vide if (lexicons.length === 0) { log("âš ï¸ Aucun lexique à afficher."); lexiquesContainer.textContent = "Aucun lexique disponible."; return; } + // Parcourt chaque lexique pour créer et afficher les éléments correspondants for (const { lexiconName, lexiconId, active } of lexicons) { + // Vérifie si le lexique est déjà affiché if (lexiquesContainer.querySelector(`div[data-lexicon-id="${lexiconId}"]`)) { - continue; // Si oui, passez à l'itération suivante + continue; } + // Crée un conteneur pour le lexique const lexiqueDiv = document.createElement("div"); lexiqueDiv.className = "lexique-item"; + // Récupère la couleur associée au lexique et crée un cercle de couleur const color = await getColorForLexicon(lexiconId); const circleIcon = createColorCircle(color, 24); + // Crée un conteneur pour l'icône de couleur const iconDiv = document.createElement("div"); iconDiv.className = "lexique-icon"; iconDiv.appendChild(circleIcon); + // Crée un élément pour le nom du lexique const labelSpan = document.createElement("span"); labelSpan.className = "lexique-label"; labelSpan.textContent = lexiconName; @@ -332,7 +406,7 @@ async function displayLexiconsWithCheckbox(lexicons) { highlightTooltip.className = "tooltip"; highlightTooltip.textContent = "Activer/Désactiver le surlignage des mots du lexique"; - // Mise à jour du gestionnaire d'événements + // Mise à jour du gestionnaire d'événements pour le bouton de surlignage highlightButton.addEventListener("click", async () => { let currentState = highlightButton.dataset.active === "true"; let newState = !currentState; @@ -341,7 +415,7 @@ async function displayLexiconsWithCheckbox(lexicons) { highlightButton.dataset.active = newState ? "true" : "false"; highlightButton.classList.toggle("active", newState); - // Sauvegarder l'état dans le storage local + // Sauvegarde l'état dans le stockage local const activeLexicons = Array.from(document.querySelectorAll('.lexique-highlight-toggle[data-active="true"]')) .map(btn => parseInt(btn.dataset.lexiconId)); await browser.storage.local.set({ activeLexicons }); @@ -350,6 +424,7 @@ async function displayLexiconsWithCheckbox(lexicons) { } }); + // Ajoute les éléments au conteneur du lexique highlightButton.appendChild(feutreIcon); highlightButton.appendChild(highlightTooltip); lexiqueDiv.appendChild(iconDiv); @@ -359,6 +434,7 @@ async function displayLexiconsWithCheckbox(lexicons) { lexiquesContainer.appendChild(lexiqueDiv); } + // Ajuste la position des tooltips après un court délai setTimeout(() => { const menu = document.getElementById("menu"); if (!menu) return; @@ -372,6 +448,7 @@ async function displayLexiconsWithCheckbox(lexicons) { tooltip.style.transform = 'translateX(-50%) translateY(-5px)'; const tooltipRect = tooltip.getBoundingClientRect(); + // Ajuste la position du tooltip si elle déborde du menu if (tooltipRect.left < menuRect.left) { const overflowLeft = menuRect.left - tooltipRect.left; tooltip.style.transform = `translateX(calc(-100% + ${overflowLeft}px)) translateY(-5px)`; @@ -383,18 +460,23 @@ async function displayLexiconsWithCheckbox(lexicons) { }, 100); } +/** + * Met à jour l'affichage des lexiques. + */ async function updateLexiconsDisplay() { - const token = await getAuthTokenFromStorage(); // Récupérer le token d'authentification + const token = await getAuthTokenFromStorage(); if (!token) { console.warn("âš ï¸ Aucun token d'authentification disponible."); return; } - const lexicons = await getLexicons(token); // Récupérer les lexiques - await displayLexiconsWithCheckbox(lexicons); // Mettre à jour l'affichage des lexiques + const lexicons = await getLexicons(token); + await displayLexiconsWithCheckbox(lexicons); } -// Ajouter une fonction pour restaurer l'état des boutons au chargement +/** + * Restaure l'état des boutons de surlignage au chargement. + */ async function restoreHighlightingState() { try { const { activeLexicons } = await browser.storage.local.get("activeLexicons"); @@ -412,6 +494,9 @@ async function restoreHighlightingState() { } } +/** + * Initialise la boîte modale pour l'affichage des définitions. + */ function initModal() { log("initModal appelé"); const modalOverlay = document.getElementById("modalOverlay"); @@ -433,27 +518,44 @@ function initModal() { if (event.target === modalOverlay) closeDefinitionPopup(); }); } - document.addEventListener("DOMContentLoaded", initModal); -// ───────────────────────────────────────────────────────────────────────────── -// â–Œ Bouton de connexion/déconnexion -// ───────────────────────────────────────────────────────────────────────────── -async function handleAuthButtonClick() { - await browser.runtime.sendMessage({ action: "toggleAuth" }); - await refreshSidebarState(); - const messageContainer = document.getElementById("messageContainer"); - if (messageContainer) { - messageContainer.style.display = "block"; - messageContainer.innerHTML = "Veuillez vous connecter pour utiliser l'extension."; +/** + * Bascule l'état de surlignage d'un lexique. + * @param {number} lexiconId - L'ID du lexique à basculer. + * @param {boolean} isActive - Indique si le surlignage doit être activé ou désactivé. + */ +async function toggleLexiconHighlight(lexiconId, isActive) { + try { + const button = document.querySelector(`button[data-lexicon-id="${lexiconId}"]`); + if (button) { + button.dataset.active = isActive.toString(); + button.classList.toggle('active', isActive); + } + + await browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + browser.tabs.sendMessage(tabs[0].id, { + command: isActive ? "activate-highlighting" : "deactivate-highlighting", + lexiconId: lexiconId + }); + }); + + log(`✅ Surlignage ${isActive ? 'activé' : 'désactivé'} pour le lexique ${lexiconId}`); + } catch (error) { + log(`⌠Erreur lors du toggle du surlignage pour le lexique ${lexiconId}:`, error); } } // ───────────────────────────────────────────────────────────────────────────── // â–Œ Ajout d'un mot au(x) lexique(s) // ───────────────────────────────────────────────────────────────────────────── +/** + * Gère le clic sur le bouton d'ajout de mot. + * Vérifie le token et le mot sélectionné, puis ajoute le mot aux lexiques sélectionnés. + */ async function handleAddWordClick() { openBlock("menuContent"); + // 1) Vérifier la présence du token et du mot if (!authToken) { console.warn("âš ï¸ Pas de token d'authentification : impossible d'ajouter le mot."); @@ -463,10 +565,13 @@ async function handleAddWordClick() { const selectedWordElement = document.getElementById("motSelectionne"); const lexiconResultElement = document.getElementById("lexiconResult"); + // Vérifie si l'élément contenant le mot sélectionné existe if (!selectedWordElement) { console.warn("âš ï¸ Ã‰lément #motSelectionne introuvable."); return; } + + // Récupère le mot sélectionné et le "nettoie" const selectedWord = selectedWordElement.textContent.trim(); if (!selectedWord || selectedWord === "Aucun mot sélectionné") { console.warn("âš ï¸ Aucun mot à ajouter."); @@ -478,6 +583,7 @@ async function handleAddWordClick() { const checkboxList = document.querySelectorAll("#lexiques .lexique-checkbox:checked"); const selectedLexiconIds = Array.from(checkboxList).map(cb => parseInt(cb.dataset.lexiconId, 10)); + // Vérifie si au moins un lexique a été sélectionné if (selectedLexiconIds.length === 0) { console.warn("âš ï¸ Aucun lexique sélectionné."); if (lexiconResultElement) lexiconResultElement.textContent = "Veuillez cocher au moins un lexique."; @@ -493,6 +599,7 @@ async function handleAddWordClick() { } const existingLexiconIds = new Set(); + // Vérifie si le mot existe déjà dans les lexiques sélectionnés if (Array.isArray(definitions)) { for (const def of definitions) { if (selectedLexiconIds.includes(def.lexiconId)) { @@ -501,7 +608,7 @@ async function handleAddWordClick() { } } - // 4) Déterminer les lexiques où ajouter le mot (ceux qui n'ont pas déjà le mot) + // 4) Déterminer les lexiques où ajouter le mot const lexiconsToAdd = selectedLexiconIds.filter(id => !existingLexiconIds.has(id)); // Si le mot existe déjà dans certains lexiques, on affiche le message @@ -516,6 +623,7 @@ async function handleAddWordClick() { } } + // Si aucun lexique n'est disponible pour ajouter le mot, on sort de la fonction if (lexiconsToAdd.length === 0) { return; } @@ -551,21 +659,22 @@ async function handleAddWordClick() { } } - // ───────────────────────────────────────────────────────────────────────────── // â–Œ Réception des messages // ───────────────────────────────────────────────────────────────────────────── browser.runtime.onMessage.addListener(async (message) => { - log("📩 Message reçu dans sidebar.js :", message); + log("Message reçu dans sidebar.js :", message); + // Vérifie si le message contient une action if (message.action) switch (message.action) { case "refreshUI": - log("🔄 Demande de rafraîchissement de la barre latérale."); + log("Demande de rafraîchissement de la barre latérale."); await refreshSidebarState(); break; case "mot_selectionne": + // Gère le mot sélectionné if (message.selectedText) { log("ðŸ–‹ï¸ Mot sélectionné :", message.selectedText); const selectedWordElement = document.getElementById("motSelectionne"); @@ -583,20 +692,23 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "getDefinition": + // Recherche des définitions pour le mot sélectionné if (message.selectedText) { - log("📖 Recherche des définitions pour :", message.selectedText); + log("Recherche des définitions pour :", message.selectedText); openBlock("definitionContent"); await window.showDefinitions(message.selectedText); } break; case "showDefinitions": + // Affiche les définitions reçues if (Array.isArray(message.definitions)) { window.displayDefinitions(message.definitions); } break; case "fetchWiktionaryDefinitionResponse": + // Gère la réponse de définition du Wiktionnaire if (message.selectedText) { log(`📖 Réception de la définition du Wiktionnaire pour '${message.selectedText}'`); window.displayDefinitions(message.definitions); @@ -604,11 +716,13 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "showLexiconResult": - log("📚 Résultat des lexiques reçus :", message.lexicons); + // Affiche le résultat des lexiques reçus + log("Résultat des lexiques reçus :", message.lexicons); window.displayLexiconResults(message.lexicons); break; case "addWordResult": + // Met à jour le résultat de l'ajout de mot const lexiconResultElement = document.getElementById("lexiconResult"); if (lexiconResultElement) { lexiconResultElement.innerHTML = message.lexicons; @@ -616,10 +730,12 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "addToLexicon": + // Gère l'ajout d'un mot au lexique handleAddWordClick(); break; case "openLexiconBlock": + // Ouvre le bloc des lexiques openBlock("menuContent"); break; @@ -630,8 +746,8 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "updateUI": + // Met à jour l'interface utilisateur en fonction de l'état de l'extension if (!message.extensionActive) { - // Fermer tous les blocs hideBlocks(true); } else { hideBlocks(false); @@ -643,17 +759,21 @@ browser.runtime.onMessage.addListener(async (message) => { return; case "saveToken": + // Sauvegarde le token d'authentification authToken = message.token; break; case "closeSidebarBlocks": + // Masque les blocs de la barre latérale hideBlocks(true); break; } + // Vérifie si le message contient une commande if (message.command) { switch (message.command) { case "activate-highlighting": + // Active le surlignage pour le lexique spécifié const highlightButton = document.querySelector(`button[data-lexicon-id="${message.lexiconId}"]`); if (highlightButton) { highlightButton.dataset.active = "true"; @@ -662,6 +782,7 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "deactivate-highlighting": + // Désactive le surlignage pour le lexique spécifié const highlightButtonOff = document.querySelector(`button[data-lexicon-id="${message.lexiconId}"]`); if (highlightButtonOff) { highlightButtonOff.dataset.active = "false"; @@ -673,7 +794,8 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "register-highlighting-script": - log("✅ Script de surlignage enregistré"); + // Enregistre le script de surlignage et met à jour le token d'authentification + log("Script de surlignage enregistré"); browser.runtime.sendMessage({ command: "updateAuthToken", token: authToken @@ -681,6 +803,7 @@ browser.runtime.onMessage.addListener(async (message) => { break; case "updateAuthToken": + // Met à jour le token d'authentification authToken = message.token; window.authToken = message.token; break; @@ -695,16 +818,22 @@ browser.runtime.onMessage.addListener(async (message) => { // â–Œ DOMContentLoaded // ───────────────────────────────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", async () => { - log("📦 DOM entièrement chargé. Initialisation de la sidebar."); + log("DOM entièrement chargé. Initialisation de la sidebar."); + + // Récupère le token d'authentification et l'assigne à l'objet window authToken = await getAuthTokenFromStorage(); window.authToken = authToken; + + // Actualise l'état de la barre latérale await refreshSidebarState(); + // Gère le clic sur le bouton d'authentification const authButton = document.getElementById("auth-button"); if (authButton) { authButton.addEventListener("click", handleAuthButtonClick); } + // Gère le clic sur le bouton de recherche de définition const chercherDefButton = document.querySelector("#chercherDef"); if (chercherDefButton) { chercherDefButton.addEventListener("click", async () => { @@ -717,13 +846,13 @@ document.addEventListener("DOMContentLoaded", async () => { console.warn("âš ï¸ Aucun mot sélectionné pour la recherche."); } }); - log("✅ Bouton #chercherDef détecté dans le DOM."); + log("Bouton #chercherDef détecté dans le DOM."); chercherDefButton.style.pointerEvents = "auto"; chercherDefButton.style.display = "block"; chercherDefButton.style.visibility = "visible"; chercherDefButton.disabled = false; } else { - log("⌠ERREUR : Bouton #chercherDef introuvable."); + log("ERREUR : Bouton #chercherDef introuvable."); } // Écouteur pour la case à cocher "toggle-definitions" @@ -742,7 +871,7 @@ document.addEventListener("DOMContentLoaded", async () => { }); } - // Bouton "Ajouter le mot sélectionné" + // Gère le clic sur le bouton "Ajouter le mot sélectionné" const addWordButton = document.getElementById("add-word-button"); if (addWordButton) { addWordButton.addEventListener("click", handleAddWordClick); @@ -750,7 +879,7 @@ document.addEventListener("DOMContentLoaded", async () => { console.warn("âš ï¸ Bouton #add-word-button introuvable dans le DOM."); } - // Écouteur pour la case à cocher "toggle-definitions" + // Gère les boutons de bascule pour afficher/masquer les blocs const toggleButtons = document.querySelectorAll(".toggle-btn"); toggleButtons.forEach((btn) => { if (!btn.textContent.trim()) { @@ -767,10 +896,12 @@ document.addEventListener("DOMContentLoaded", async () => { } }); + // Masque tous les blocs au chargement document.querySelectorAll('.block-content').forEach(block => { block.classList.add('hidden'); }); + // Réinitialise les boutons de bascule document.querySelectorAll('.toggle-btn').forEach(btn => { btn.textContent = '+'; btn.style.fontSize = '15px'; @@ -788,41 +919,18 @@ document.addEventListener("DOMContentLoaded", async () => { }); }); - document.querySelectorAll('.toggle-btn').forEach(btn => { - btn.addEventListener('click', function() { - const blockContent = this.parentElement.nextElementSibling; - if (blockContent.classList.contains('hidden')) { - blockContent.classList.remove('hidden'); - this.textContent = '–'; - } else { - blockContent.classList.add('hidden'); - this.textContent = '+'; - } - }); - }); - -async function toggleLexiconHighlight(lexiconId, isActive) { - try { - const button = document.querySelector(`button[data-lexicon-id="${lexiconId}"]`); - if (button) { - button.dataset.active = isActive.toString(); - button.classList.toggle('active', isActive); +// Gère les clics sur les boutons de bascule +document.querySelectorAll('.toggle-btn').forEach(btn => { + btn.addEventListener('click', function() { + const blockContent = this.parentElement.nextElementSibling; + if (blockContent.classList.contains('hidden')) { + blockContent.classList.remove('hidden'); + this.textContent = '–'; + } else { + blockContent.classList.add('hidden'); + this.textContent = '+'; } + }); +}); - await browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - browser.tabs.sendMessage(tabs[0].id, { - command: isActive ? "activate-highlighting" : "deactivate-highlighting", - lexiconId: lexiconId - }); - }); - - log(`✅ Surlignage ${isActive ? 'activé' : 'désactivé'} pour le lexique ${lexiconId}`); - } catch (error) { - log(`⌠Erreur lors du toggle du surlignage pour le lexique ${lexiconId}:`, error); - } -} -async function checkAnalysisStatus() { - const { extensionActive } = await browser.storage.local.get("extensionActive"); - return extensionActive; // Retourne true si l'analyse est activée, sinon false -} diff --git a/src/utils/api.js b/src/utils/api.js index 58695258e268059be0e86ce3f9c4232309c96903..efcd5749ab1841bcde21902e6142c221a7067bfa 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,4 +1,5 @@ -log("✅ api.js chargé correctement"); +log("api.js chargé."); + window.authToken = null; // ───────────────────────────────────────────────────────────────────────────── @@ -27,21 +28,27 @@ document.addEventListener("mouseup", () => { * @throws {Error} - En cas d'échec. */ async function callApi(url, authToken = null, method = 'GET', data = null) { + // Définition des en-têtes de la requête const headers = { 'Content-Type': 'application/json' }; if (authToken) headers.Authorization = `Bearer ${authToken}`; + // Options de la requête fetch const fetchOptions = { method, headers }; log("Envoi de la requête vers :", url); + // Si des données sont fournies, les ajouter au corps de la requête if (data) { log("Body JSON :", JSON.stringify(data, null, 2)); fetchOptions.body = JSON.stringify(data); } try { + // Effectuer la requête fetch const response = await fetch(url, fetchOptions); + // Vérifier si la réponse est correcte if (!response.ok) { throw new Error(`⌠Erreur API (${response.status}): ${response.statusText}`); } + // Retourner la réponse en JSON return await response.json(); } catch (error) { log(`🚨 Erreur lors de l'appel API [${url}]:`, error); @@ -71,6 +78,9 @@ async function getLexicons(authToken) { /** * Récupère les entrées d'un lexique donné. + * @param {string} authToken - Le token d'authentification. + * @param {string} lexiconId - L'ID du lexique dont on veut récupérer les entrées. + * @returns {Promise<any[]>} - Liste des entrées du lexique. */ async function getLexiconEntries(authToken, lexiconId) { const url = `https://babalex.lezinter.net/api/lexicon/entries/${lexiconId}`; @@ -79,12 +89,15 @@ async function getLexiconEntries(authToken, lexiconId) { /** * Récupère toutes les graphies présentes dans tous les lexiques de l'utilisateur. + * @param {string} authToken - Le token d'authentification. + * @returns {Promise<object>} - Un objet contenant les graphies par lexique. */ async function getAllLexiconWords(authToken) { try { // 1) Récupération de la liste des lexiques const lexicons = await getLexicons(authToken); + // Vérification si la liste des lexiques est valide if (!Array.isArray(lexicons) || lexicons.length === 0) { console.warn("âš ï¸ Aucun lexique retourné par l'API pour ces paramètres."); return {}; @@ -94,12 +107,14 @@ async function getAllLexiconWords(authToken) { const allGraphiesByLexicon = {}; for (const lexicon of lexicons) { + // Récupération des entrées pour le lexique actuel const entries = await getLexiconEntries(authToken, lexicon.id); // Vérification que entries est bien un tableau if (!Array.isArray(entries)) { console.warn(`âš ï¸ Format invalide pour les entrées du lexique ${lexicon.id}:`, entries); continue; } + // Extraction des graphies des entrées const allGraphies = entries.map(entry => entry.graphy); // Création d'un libellé unique pour le lexique @@ -108,13 +123,14 @@ async function getAllLexiconWords(authToken) { ? `Lexique personnel (${lexicon.user?.pseudo || "Inconnu"}) [${lexicon.id}]` : `Lexique de groupe (${lexicon.group?.name || "Inconnu"}) [${lexicon.id}]`; + // Stockage des graphies par lexique allGraphiesByLexicon[lexiconName] = allGraphies; } - log("✅ Toutes les graphies récupérées :", allGraphiesByLexicon); + log("Toutes les graphies récupérées :", allGraphiesByLexicon); return allGraphiesByLexicon; } catch (error) { - log("⌠Erreur lors de la récupération des graphies des lexiques :", error); + log("Erreur lors de la récupération des graphies des lexiques :", error); return {}; } } @@ -122,25 +138,31 @@ async function getAllLexiconWords(authToken) { // ───────────────────────────────────────────────────────────────────────────── // â–Œ Récupération de définition du Wiktionnaire // ───────────────────────────────────────────────────────────────────────────── - /** * Récupère une définition du Wiktionnaire. + * @param {string} word - Le mot dont on veut la définition. + * @returns {Promise<string[]>} - Une promesse contenant la définition trouvée. */ async function getWiktionaryDefinition(word) { try { + // Construction de l'URL pour l'API du Wiktionnaire avec le mot spécifié const url = `https://fr.wiktionary.org/w/api.php?action=query&format=json&origin=*&prop=extracts&exintro=true&titles=${encodeURIComponent(word)}`; + // Envoi de la requête fetch à l'API const response = await fetch(url); if (!response.ok) { throw new Error(`Erreur API Wiktionnaire: ${response.statusText}`); } + // Traitement de la réponse JSON const data = await response.json(); const pages = data.query?.pages; + // Récupération de la première page de résultats const page = pages ? Object.values(pages)[0] : null; + // Extraction de la définition ou message par défaut si aucune définition n'est trouvée const definition = page?.extract?.trim() || "Aucune définition trouvée."; log(`📖 Définition trouvée pour '${word}':`, definition); - return [definition]; + return [definition]; // Retourner la définition sous forme de tableau } catch (error) { log("Erreur lors de la récupération du Wiktionnaire :", error); return ["Erreur : " + error.message]; @@ -170,6 +192,7 @@ async function AddWord(authToken, selectedWord, lexiconIds, force = false) { } const url = "https://babalex.lezinter.net/api/entry/create"; + // Corps de la requête contenant le mot et les lexiques cibles const body = { graphy: selectedWord, force, @@ -179,8 +202,6 @@ async function AddWord(authToken, selectedWord, lexiconIds, force = false) { return callApi(url, authToken, "POST", body); } - - // ───────────────────────────────────────────────────────────────────────────── // â–Œ Exposition des fonctions pour un usage global // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/utils/definitions.js b/src/utils/definitions.js index a510319deae5c431c4d8c97b55d72b50fda13dde..9b930d71144d0642751c61b6d6fdadf991fda267 100644 --- a/src/utils/definitions.js +++ b/src/utils/definitions.js @@ -1,19 +1,14 @@ // ───────────────────────────────────────────────────────────────────────────── -// â–Œ Fonctions pour récupérer/afficher les définitions +// â–Œ Récupération des définitions (lexiques + Wiktionnaire) // ───────────────────────────────────────────────────────────────────────────── window.lexiconMap = new Map(); /** -* Récupère les définitions d'un mot dans les lexiques de l'utilisateur (ex. user_id=4), -* en effectuant un appel global à l'API pour le mot recherché, puis en filtrant -* les entrées pour ne conserver que celles dont le lexicon.id est présent dans la liste -* des lexiques de l'utilisateur. -* -* Retourne un tableau d'objets { source, text } où: -* - source = nom du lexique (ou "Lexique #ID") -* - text = texte de la définition -*/ + * Récupère les définitions d'un mot dans les lexiques de l'utilisateur. + * @param {string} word - Le mot dont on veut les définitions. + * @returns {Promise<object[]>} - Un tableau d'objets contenant les définitions. + */ async function fetchLexiconDefinitions(word) { try { log(`🔠Recherche des définitions de '${word}' dans les lexiques de l'utilisateur...`); @@ -48,7 +43,7 @@ async function fetchLexiconDefinitions(word) { : `Lexique de groupe : ${lex.group?.name || "Inconnu"}`; lexiconMap.set(lex.id, lexiconName); }); - log("📌 LexiconMap :", lexiconMap); + log("LexiconMap :", lexiconMap); // 2) Pour chaque lexique, rechercher le mot en ajoutant target_lex const definitionsPromises = userLexicons.map(async (lex) => { @@ -120,18 +115,21 @@ async function fetchLexiconDefinitions(word) { }); }); }); + log("Résultat final filtré :", allDefinitions); - return allDefinitions; - } catch (error) { - log("⌠Erreur générale lors de la récupération des définitions :", error); - return []; - } - } + return allDefinitions; + } catch (error) { + log("Erreur générale lors de la récupération des définitions :", error); + return []; + } +} /** -* Récupère la définition d'un mot depuis le Wiktionnaire (fr). -* Retourne un tableau d'objets : [{ source: 'Wiktionnaire', text: '...' }] -*/ + * Récupère la définition d'un mot depuis le Wiktionnaire (fr). + * Retourne un tableau d'objets : [{ source: 'Wiktionnaire', text: '...' }] + * @param {string} word - Le mot dont on veut la définition. + * @returns {Promise<object[]>} - Un tableau d'objets contenant la définition. + */ async function fetchWiktionaryDefinition(word) { try { const result = await browser.storage.local.get("accessToken"); @@ -318,20 +316,18 @@ function formatDefinitionData(apiResponse) { return formattedData; } - - - // ───────────────────────────────────────────────────────────────────────────── -// â–Œ Affichage des définitions dans la barre latérale +// â–Œ Affichage des définitions dans la barre latérale (lexiques + Wiktionnaire) // ───────────────────────────────────────────────────────────────────────────── const MAX_LENGTH = 200; /** -* Affiche les définitions dans la barre latérale. -*/ + * Affiche les définitions dans la barre latérale. + * @param {object[]} definitions - Les définitions à afficher. + */ function displayDefinitions(definitions) { - log("📖 Affichage des définitions reçues :", definitions); + log("Affichage des définitions reçues :", definitions); if (!Array.isArray(definitions)) return; const mesLexiquesList = document.getElementById("mesLexiquesList"); @@ -522,14 +518,15 @@ async function combineDefinitions(word) { const allDefinitions = [...lexiconDefinitions, ...wiktionaryDefinitions]; - log("📚 [combineDefinitions] Résultat fusionné :", allDefinitions); + log("[combineDefinitions] Résultat fusionné :", allDefinitions); return allDefinitions; } /** -* Récupère et affiche toutes les définitions (lexiques + Wiktionnaire). -*/ + * Récupère et affiche toutes les définitions (lexiques + Wiktionnaire). + * @param {string} word - Le mot dont on veut les définitions. + */ async function showDefinitions(word) { log(`[showDefinitions] Recherche + affichage pour "${word}"...`); @@ -559,11 +556,11 @@ async function showDefinitions(word) { return allDefinitions; } catch (error) { - log("⌠[showDefinitions] Erreur : ", error); + log("[showDefinitions] Erreur : ", error); if (noDefinitionsContainer) { noDefinitionsContainer.textContent = - "⌠Une erreur est survenue lors de la récupération des définitions."; + "Une erreur est survenue lors de la récupération des définitions."; noDefinitionsContainer.style.display = "block"; } return []; @@ -571,21 +568,22 @@ async function showDefinitions(word) { } /** -* Appel direct pour récupérer les définitions d'un mot uniquement via l'API -* (sans Wiktionnaire), puis gérer l'affichage d'erreur ou non. -*/ + * Appel direct pour récupérer les définitions d'un mot uniquement via l'API + * (sans Wiktionnaire), puis gérer l'affichage d'erreur ou non. + * @param {string} word - Le mot dont on veut la définition. + */ async function fetchDefinition(word) { log(`🔠Recherche de la définition pour '${word}'...`); const noDefinitionsContainer = document.getElementById("noDefinitionsContainer"); if (!noDefinitionsContainer) { - log("⌠Élément #noDefinitionsContainer introuvable."); + log("Élément #noDefinitionsContainer introuvable."); return; } try { const definition = await fetchLexiconDefinitions(word); - log("🔠Résultat API :", definition); + log("Résultat API :", definition); if (!definition || definition.length === 0) { console.warn(`âš ï¸ Aucune définition trouvée pour '${word}'`); @@ -595,13 +593,14 @@ async function fetchDefinition(word) { noDefinitionsContainer.style.display = "none"; } catch (error) { - log("⌠Erreur lors de la récupération de la définition :", error); + log("Erreur lors de la récupération de la définition :", error); noDefinitionsContainer.style.display = "block"; } } /** -* Affiche les lexiques où le mot sélectionné est présent. + * Affiche les lexiques où le mot sélectionné est présent. + * @param {object[]} lexicons - Les lexiques où le mot est présent. */ function displayLexiconResults(lexicons) { const resultDiv = document.getElementById("lexiconResult"); @@ -610,7 +609,7 @@ function displayLexiconResults(lexicons) { resultDiv.innerHTML = ""; if (!lexicons || lexicons.length === 0) { - resultDiv.textContent = "⌠Ce mot n'est présent dans aucun lexique."; + resultDiv.textContent = "Ce mot n'est présent dans aucun lexique."; return; } const title = document.createElement("p"); @@ -648,10 +647,12 @@ function displayLexiconResults(lexicons) { window.fetchLexiconDefinitions = fetchLexiconDefinitions; window.fetchWiktionaryDefinition = fetchWiktionaryDefinition; +window.wikiApiResponse = wikiApiResponse; +window.formatDefinitionData = formatDefinitionData; window.displayDefinitions = displayDefinitions; -window.openDefinitionPopup = openDefinitionPopup; -window.closeDefinitionPopup = closeDefinitionPopup; window.combineDefinitions = combineDefinitions; window.showDefinitions = showDefinitions; window.fetchDefinition = fetchDefinition; window.displayLexiconResults = displayLexiconResults; +window.openDefinitionPopup = openDefinitionPopup; +window.closeDefinitionPopup = closeDefinitionPopup; diff --git a/src/utils/highlighting.js b/src/utils/highlighting.js index 4a673e9a1849f7f040d23b055fd09f39948b448e..1851fda0fdb44aac5f4f59bb3d4aee12d48209c0 100644 --- a/src/utils/highlighting.js +++ b/src/utils/highlighting.js @@ -1,55 +1,87 @@ -// Variables globales +// ───────────────────────────────────────────────────────────── +// â–Œ Configuration initiale, logs et gestion des erreurs +// ───────────────────────────────────────────────────────────── +log("highlighting.js chargé."); window.activeLexiconIds = window.activeLexiconIds || new Set(); -// Logs immédiats -log("🔵 DÉBUT DU FICHIER highlighting.js"); -try { - log("✅ highlighting.js chargé"); -} catch (e) { - log("⌠Erreur avec la fonction log:", e); -} - // Vérification de l'environnement -log("🔠Vérification de l'environnement:", { +log("Vérification de l'environnement:", { hasBrowser: typeof browser !== 'undefined', windowLocation: window.location.href }); // Gestion globale des erreurs window.onerror = function(msg, url, line, col, error) { - log("🔴 Erreur globale:", { message: msg, url: url, line: line, col: col, error: error }); + log("Erreur globale:", { message: msg, url: url, line: line, col: col, error: error }); return false; }; -// Fonction d'initialisation du token depuis le stockage local +// ───────────────────────────────────────────────────────────── +// â–Œ Authentification et gestion du token +// ───────────────────────────────────────────────────────────── +/** + * Récupère le token d'authentification depuis le stockage local. + * @returns {Promise<boolean>} true si le token est récupéré, false sinon. + */ async function initAuthToken() { try { const { accessToken } = await browser.storage.local.get("accessToken"); if (accessToken) { window.authToken = accessToken; authToken = accessToken; - log("🔑 Token récupéré depuis le stockage local"); + log("Token récupéré depuis le stockage local"); return true; } else { log("âš ï¸ Aucun token trouvé dans le stockage local"); return false; } } catch (error) { - log("⌠Erreur lors de la récupération du token:", error); + log("Erreur lors de la récupération du token:", error); return false; } } initAuthToken(); -// Écoute de mise à jour du token +// Mise à jour du token via messages du background browser.runtime.onMessage.addListener((message) => { if (message.command === "updateAuthToken" && message.token) { window.authToken = message.token; authToken = message.token; - log("🔑 Token mis à jour via message:", !!message.token); + log("Token mis à jour via message:", !!message.token); + } +}); + +// ───────────────────────────────────────────────────────────── +// â–Œ Gestion des événements globaux +// ───────────────────────────────────────────────────────────── +// Réinitialisation du surlignage lors du retour sur la page +document.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'visible' && window.highlightingActive && activeLexiconIds.size > 0) { + log("Page redevenue visible, réinitialisation du surlignage"); + removeAllHighlights(); + await updateLexiconCache(); + highlightVisibleContent(); + attachMutationObserver(); + } +}); +window.addEventListener('pageshow', async () => { + if (window.highlightingActive && activeLexiconIds.size > 0) { + log("Page affichée (pageshow), réinitialisation du surlignage"); + removeAllHighlights(); + await updateLexiconCache(); + highlightVisibleContent(); + attachMutationObserver(); } }); +// Enregistrement du script auprès du background +log("Enregistrement du script auprès du background"); +browser.runtime.sendMessage({ command: "register-highlighting-script" }); +log("Initialisation de highlighting.js"); + +// ───────────────────────────────────────────────────────────── +// â–Œ Logique de surlignage et gestion des mutations DOM +// ───────────────────────────────────────────────────────────── (function () { try { if (window.hasRun) { @@ -64,160 +96,236 @@ browser.runtime.onMessage.addListener((message) => { window.highlightingActive = false; let observer = null; - // Gestion de la visibilité/page show pour réappliquer le surlignage - document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible' && window.highlightingActive && activeLexiconIds.size > 0) { - log("📄 Page redevenue visible, réinitialisation du surlignage"); - removeAllHighlights(); - await updateLexiconCache(); - highlightVisibleContent(); - attachMutationObserver(); - } - }); - window.addEventListener('pageshow', async () => { - if (window.highlightingActive && activeLexiconIds.size > 0) { - log("📄 Page affichée (pageshow), réinitialisation du surlignage"); - removeAllHighlights(); - await updateLexiconCache(); - highlightVisibleContent(); - attachMutationObserver(); - } - }); - - // Gestion des messages reçus du background - browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - log("📨 Message reçu:", message, "Context:", { - highlightingActive, - activeLexiconIds: Array.from(activeLexiconIds), - hasAuthToken: !!window.authToken, - hasGetAllLexiconWords: !!window.getAllLexiconWords - }); - - if (message.command === "activate-highlighting") { - log(`🎯 Activation du surlignage pour le lexique ${message.lexiconId}`); - startHighlighting(message.lexiconId) - .then(() => { - window.highlightingActive = true; - sendResponse(true); - }) - .catch(error => { - log("⌠Erreur lors de l'activation:", error); - sendResponse(false); - }); - return true; - } - - if (message.command === "deactivate-highlighting") { - log(`🚫 Désactivation du surlignage pour le lexique ${message.lexiconId}`); - stopHighlighting(message.lexiconId) - .then(() => { - if (activeLexiconIds.size === 0) { - window.highlightingActive = false; - } - sendResponse(true); - }) - .catch(error => { - log("⌠Erreur lors de la désactivation:", error); - sendResponse(false); - }); - return true; - } - - return false; - }); - - log("📡 Enregistrement du script auprès du background"); - browser.runtime.sendMessage({ command: "register-highlighting-script" }); + // ─────────────────────────────── + // â–Œ Fonctions utilitaires + // ─────────────────────────────── + /** + * Échappe les caractères spéciaux pour utilisation dans une expression régulière + * @param {string} string - La chaîne à échapper + * @returns {string} La chaîne échappée + */ + function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } - log("🚀 Initialisation de highlighting.js"); + /** + * Extrait l'ID d'un lexique depuis son nom + * @param {string} lexiconName - Le nom du lexique + * @returns {number|null} L'ID du lexique ou null si l'extraction échoue + */ + function getLexiconIdFromName(lexiconName) { + const match = lexiconName.match(/\[(\d+)\]$/); + const id = match ? parseInt(match[1]) : null; + log(`ðŸ·ï¸ Extraction de l'ID depuis '${lexiconName}': ${id}`); + return id; + } - // Fonction asynchrone pour mettre à jour le style d'un élément surligné en fonction des lexiques qui le concernent + // ─────────────────────────────── + // â–Œ Gestion du style de surlignage + // ─────────────────────────────── + /** + * Met à jour le style du surlignage pour un élément spécifié + * @param {Element} span - L'élément à surligner + * @param {Array} lexIds - Les identifiants des lexiques à surligner + * @returns {Promise<void>} + */ async function updateHighlightStyle(span, lexIds) { if (!lexIds || lexIds.length === 0) { + // Définit une couleur de fond par défaut si aucun lexique n'est actif span.style.backgroundColor = "rgba(255, 255, 0, 0.3)"; span.style.backgroundImage = ""; return; } + // Si un seul lexique est actif if (lexIds.length === 1) { + // Récupère la couleur hexadécimale pour le lexique const hexColor = await getColorForLexicon(lexIds[0]); const rgbaColor = hexToRgba(hexColor, 0.3); span.style.backgroundColor = rgbaColor; span.style.backgroundImage = ""; } else { + // Si plusieurs lexiques sont actifs, récupère les couleurs pour chacun const hexColors = await Promise.all(lexIds.map(id => getColorForLexicon(id))); const colors = hexColors.map(hex => hexToRgba(hex, 0.3)); - const n = colors.length; - let stops = []; + const n = colors.length; // Nombre de couleurs + let stops = []; // Tableau pour stocker les arrêts de dégradé for (let i = 0; i < n; i++) { + // Calcule les positions de début et de fin pour chaque couleur dans le dégradé const start = ((100 * i) / n).toFixed(2) + '%'; const end = ((100 * (i + 1)) / n).toFixed(2) + '%'; stops.push(`${colors[i]} ${start}, ${colors[i]} ${end}`); } + // Crée un dégradé linéaire avec les couleurs calculées const gradient = `linear-gradient(90deg, ${stops.join(', ')})`; - span.style.backgroundImage = gradient; - + span.style.backgroundImage = gradient; // Applique le dégradé comme image de fond } } - // Vérifier si un mot appartient à un lexique (à l'aide de la cache) + // ─────────────────────────────── + // â–Œ Gestion du cache des lexiques + // ─────────────────────────────── + /** + * Met à jour le cache des lexiques + * @returns {Promise<boolean>} true si la mise à jour est réussie, false sinon + */ + async function updateLexiconCache() { + log("updateLexiconCache - Début avec contexte:", { + authToken: !!window.authToken, + getAllLexiconWords: !!window.getAllLexiconWords, + activeLexiconIds: Array.from(activeLexiconIds) + }); + let allWords; + try { + if (!window.authToken) { + throw new Error("Pas de token d'authentification"); + } + if (typeof window.getAllLexiconWords !== 'function') { + log("âš ï¸ getAllLexiconWords n'est pas une fonction"); + log("Type de getAllLexiconWords:", typeof window.getAllLexiconWords); + log("Contenu de window.getAllLexiconWords:", window.getAllLexiconWords); + throw new Error("getAllLexiconWords n'est pas disponible"); + } + log("Appel de getAllLexiconWords..."); + // Appelle la fonction pour récupérer tous les mots de lexique + allWords = await window.getAllLexiconWords(window.authToken); + log("Réponse de getAllLexiconWords:", allWords); + if (!allWords || typeof allWords !== 'object') { + throw new Error(`Format de données invalide: ${JSON.stringify(allWords)}`); + } + lexiconWordsCache.clear(); + if (Object.keys(allWords).length === 0) { + log("âš ï¸ Aucun lexique reçu de getAllLexiconWords"); + return false; + } + // Traite chaque lexique reçu + for (const [lexiconName, words] of Object.entries(allWords)) { + if (!Array.isArray(words)) { + console.warn(`âš ï¸ Format invalide pour le lexique ${lexiconName}:`, words); + continue; + } + // Extrait l'ID du lexique + const lexiconId = lexiconName.match(/\[(\d+)\]$/)?.[1]; + if (!lexiconId) { + console.warn(`âš ï¸ Impossible d'extraire l'ID du lexique depuis: ${lexiconName}`); + continue; + } + log(`📎 Traitement du lexique ${lexiconName} (ID: ${lexiconId})`); + // Si l'ID du lexique est actif, met à jour le cache + if (activeLexiconIds.has(Number(lexiconId))) { + lexiconWordsCache.set(lexiconId, new Set(words)); + log(`📖 Lexique ${lexiconId} chargé avec ${words.length} mots`); + } + } + log("Cache des lexiques mis à jour:", + Object.fromEntries([...lexiconWordsCache.entries()].map(([id, words]) => [id, [...words]]))); + return true; + } catch (error) { + log("Erreur dans updateLexiconCache:", { + message: error.message, + stack: error.stack, + authTokenExists: !!window.authToken, + getAllLexiconWordsType: typeof window.getAllLexiconWords, + response: allWords + }); + throw error; + } + } + + // ─────────────────────────────── + // â–Œ Vérification d'appartenance d'un mot à un lexique + // ─────────────────────────────── + /** + * Vérifie si un mot appartient à un lexique spécifié + * @param {string} lexiconId - L'identifiant du lexique + * @param {string} word - Le mot à vérifier + * @returns {boolean} true si le mot appartient au lexique, false sinon + */ function wordIsInLexicon(lexiconId, word) { const wordsSet = lexiconWordsCache.get(String(lexiconId)); return wordsSet ? wordsSet.has(word.toLowerCase()) : false; } - // Mise à jour incrémentale des éléments déjà surlignés lorsqu'un nouveau lexique est ajouté + // ─────────────────────────────── + // â–Œ Mise à jour incrémentale des surlignages pour un nouveau lexique + // ─────────────────────────────── + /** + * Met à jour les surlignages pour un nouveau lexique + * @param {string} newLexiconId - L'identifiant du nouveau lexique + * @returns {Promise<void>} + */ async function updateHighlightsForNewLexicon(newLexiconId) { const spans = document.querySelectorAll('.lexicon-highlight'); + // Parcourt chaque élément surligné for (let span of spans) { - const word = span.textContent; + const word = span.textContent; + // Vérifie si le mot appartient au nouveau lexique if (wordIsInLexicon(newLexiconId, word)) { let lexIds = []; try { lexIds = JSON.parse(span.getAttribute('data-lexicons')); } catch (e) { - lexIds = []; + lexIds = []; // Si une erreur se produit, initialise lexIds comme un tableau vide } + // Vérifie si le nouvel identifiant de lexique n'est pas déjà présent if (!lexIds.includes(newLexiconId)) { - lexIds.push(newLexiconId); + lexIds.push(newLexiconId); // Ajoute le nouvel identifiant de lexique span.setAttribute('data-lexicons', JSON.stringify(lexIds)); + // Met à jour le style de surlignage pour l'élément await updateHighlightStyle(span, lexIds); } } } } - // Fonction startHighlighting modifiée pour intégrer la mise à jour incrémentale + // ─────────────────────────────── + // â–Œ Fonctions principales : démarrage et arrêt du surlignage + // ─────────────────────────────── + /** + * Démarre le surlignage pour un lexique spécifié + * @param {string} lexiconId - L'identifiant du lexique à surligner + * @returns {Promise<boolean>} true si le surlignage est démarré, false sinon + */ async function startHighlighting(lexiconId) { try { await initAuthToken(); if (!window.authToken) { throw new Error("⌠Pas de token d'authentification disponible"); } + // Vérifie si un identifiant de lexique a été fourni if (lexiconId) { + // Vérifie si le lexique n'est pas déjà actif if (!activeLexiconIds.has(lexiconId)) { activeLexiconIds.add(lexiconId); const activeLexicons = Array.from(activeLexiconIds); + // Sauvegarde les lexiques actifs dans le stockage local await browser.storage.local.set({ activeLexicons }); - log("📊 Lexiques actifs sauvegardés:", activeLexicons); + log("Lexiques actifs sauvegardés:", activeLexicons); // Mise à jour de la cache pour inclure le nouveau lexique await updateLexiconCache(); - // Mise à jour immédiate des éléments surlignés pour intégrer le nouveau lexique + // Mise à jour immédiate des éléments surlignés await updateHighlightsForNewLexicon(lexiconId); } } + // Active le surlignage window.highlightingActive = true; highlightingActive = true; highlightVisibleContent(); + // Attache un observateur de mutations pour surveiller les changements dans le DOM attachMutationObserver(); return true; } catch (error) { - log("⌠Erreur lors du démarrage du surlignage:", error); + log("Erreur lors du démarrage du surlignage:", error); window.highlightingActive = false; highlightingActive = false; throw error; } } + /** + * Arrête le surlignage pour un lexique spécifié + * @param {string} lexiconId - L'identifiant du lexique à arrêter + * @returns {Promise<boolean>} true si l'arrêt est réussi, false sinon + */ async function stopHighlighting(lexiconId) { try { if (lexiconId) { @@ -243,87 +351,35 @@ browser.runtime.onMessage.addListener((message) => { } return true; } catch (error) { - log("⌠Erreur lors de l'arrêt du surlignage:", error); - throw error; - } - } - - // Mise à jour du cache des lexiques - async function updateLexiconCache() { - log("📥 updateLexiconCache - Début avec context:", { - authToken: !!window.authToken, - getAllLexiconWords: !!window.getAllLexiconWords, - activeLexiconIds: Array.from(activeLexiconIds) - }); - let allWords; - try { - if (!window.authToken) { - throw new Error("Pas de token d'authentification"); - } - if (typeof window.getAllLexiconWords !== 'function') { - log("âš ï¸ getAllLexiconWords n'est pas une fonction"); - log("Type de getAllLexiconWords:", typeof window.getAllLexiconWords); - log("Contenu de window.getAllLexiconWords:", window.getAllLexiconWords); - throw new Error("getAllLexiconWords n'est pas disponible"); - } - log("📥 Appel de getAllLexiconWords..."); - allWords = await window.getAllLexiconWords(window.authToken); - log("📠Réponse de getAllLexiconWords:", allWords); - if (!allWords || typeof allWords !== 'object') { - throw new Error(`Format de données invalide: ${JSON.stringify(allWords)}`); - } - lexiconWordsCache.clear(); - if (Object.keys(allWords).length === 0) { - log("âš ï¸ Aucun lexique reçu de getAllLexiconWords"); - return false; - } - for (const [lexiconName, words] of Object.entries(allWords)) { - if (!Array.isArray(words)) { - console.warn(`âš ï¸ Format invalide pour le lexique ${lexiconName}:`, words); - continue; - } - const lexiconId = lexiconName.match(/\[(\d+)\]$/)?.[1]; - if (!lexiconId) { - console.warn(`âš ï¸ Impossible d'extraire l'ID du lexique depuis: ${lexiconName}`); - continue; - } - log(`📎 Traitement du lexique ${lexiconName} (ID: ${lexiconId})`); - if (activeLexiconIds.has(Number(lexiconId))) { - lexiconWordsCache.set(lexiconId, new Set(words)); - log(`📖 Lexique ${lexiconId} chargé avec ${words.length} mots`); - } - } - log("✅ Cache des lexiques mis à jour:", - Object.fromEntries([...lexiconWordsCache.entries()].map(([id, words]) => [id, [...words]]))); - return true; - } catch (error) { - log("⌠Erreur dans updateLexiconCache:", { - message: error.message, - stack: error.stack, - authTokenExists: !!window.authToken, - getAllLexiconWordsType: typeof window.getAllLexiconWords, - response: allWords - }); + log("Erreur lors de l'arrêt du surlignage:", error); throw error; } } - // Surlignage du contenu visible + // ─────────────────────────────── + // â–Œ Surlignage du contenu visible + // ─────────────────────────────── + /** + * Surligne le contenu visible de la page + */ function highlightVisibleContent() { + // Vérifie si le surlignage est actif if (!highlightingActive) { log("â¸ï¸ Surlignage inactif, sortie"); return; } - log("🔠Début du surlignage du contenu visible"); - const BATCH_SIZE = 100; - const BATCH_DELAY = 10; // ms - const textNodes = []; + log("Début du surlignage du contenu visible"); + const BATCH_SIZE = 100; // Nombre d'éléments à traiter par lot + const BATCH_DELAY = 10; // Délai entre les traitements de lots en millisecondes + const textNodes = []; // Tableau pour stocker les nÅ“uds de texte trouvés + // Crée un TreeWalker pour parcourir les nÅ“uds de texte dans le document const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; + // Vérifie si le nÅ“ud doit être accepté ou rejeté if (!parent || parent.closest('script, style, .lexicon-highlight') || !node.textContent.trim() || getComputedStyle(parent).display === 'none') { return NodeFilter.FILTER_REJECT; } @@ -332,45 +388,60 @@ browser.runtime.onMessage.addListener((message) => { } ); let node; + // Parcourt tous les nÅ“uds de texte et les ajoute au tableau while (node = walker.nextNode()) { textNodes.push(node); } log(`🔤 ${textNodes.length} nÅ“uds de texte trouvés à traiter`); + // Fonction pour traiter le prochain lot de nÅ“uds de texte const processNextBatch = (startIndex) => { + // Vérifie si le surlignage est toujours actif et si des nÅ“uds restent à traiter if (!highlightingActive || startIndex >= textNodes.length) { - return; + return; // Sort de la fonction si le surlignage n'est pas actif ou si tous les nÅ“uds ont été traités } - const endIndex = Math.min(startIndex + BATCH_SIZE, textNodes.length); - const batch = textNodes.slice(startIndex, endIndex); - batch.forEach(processTextNode); + const endIndex = Math.min(startIndex + BATCH_SIZE, textNodes.length); // Calcule l'index de fin du lot + const batch = textNodes.slice(startIndex, endIndex); // Extrait le lot de nÅ“uds à traiter + batch.forEach(processTextNode); // Traite chaque nÅ“ud du lot + // Si des nÅ“uds restent à traiter, planifie le traitement du prochain lot if (endIndex < textNodes.length) { setTimeout(() => processNextBatch(endIndex), BATCH_DELAY); } }; - processNextBatch(0); + processNextBatch(0); // Démarre le traitement à partir du premier nÅ“ud } - // Traitement d'un nÅ“ud de texte + // ─────────────────────────────── + // â–Œ Traitement d'un "nÅ“ud de texte" pour le surlignage + // ─────────────────────────────── + /** + * Traite un "nÅ“ud de texte" pour le surlignage + * @param {Node} textNode - Le nÅ“ud de texte à traiter + */ function processTextNode(textNode) { if (activeLexiconIds.size === 0) { log("âš ï¸ Aucun lexique actif, sortie du processTextNode"); return; } - const text = textNode.textContent; - log(`🔠Traitement du texte: "${text.substring(0, 50)}..."`); - let lastIndex = 0; - let fragments = []; - const allWords = new Set(); - const matchedLexiconIdsMap = new Map(); + const text = textNode.textContent; // Récupère le texte du nÅ“ud + log(`🔠Traitement du texte: "${text.substring(0, 50)}..."`); + let lastIndex = 0; // Index de la dernière correspondance trouvée + let fragments = []; // Tableau pour stocker les parties de texte + const allWords = new Set(); // Ensemble pour stocker tous les mots à rechercher + const matchedLexiconIdsMap = new Map(); // Map pour associer les mots aux identifiants de lexiques + + // Parcourt chaque lexique dans le cache for (const [lexiconId, words] of lexiconWordsCache.entries()) { - const numericId = parseInt(lexiconId); + const numericId = parseInt(lexiconId); // Convertit l'ID du lexique en nombre log(`🔄 Vérification du lexique ${lexiconId} (ID: ${numericId})`); + // Vérifie si le lexique est actif if (activeLexiconIds.has(numericId)) { log(`✅ Lexique ${lexiconId} actif, ajout de ${words.size} mots`); + // Ajoute chaque mot à l'ensemble des mots à rechercher words.forEach(word => allWords.add(word)); + // Associe chaque mot à son identifiant de lexique words.forEach(word => { const lowerCaseWord = word.toLowerCase(); - if (!matchedLexiconIdsMap.has(lowerCaseWord)) { // Correction ici ! + if (!matchedLexiconIdsMap.has(lowerCaseWord)) { matchedLexiconIdsMap.set(lowerCaseWord, []); } matchedLexiconIdsMap.get(lowerCaseWord).push(lexiconId); @@ -378,23 +449,28 @@ browser.runtime.onMessage.addListener((message) => { } } log(`🔤 Nombre total de mots à rechercher: ${allWords.size}`); + // Vérifie si des mots sont disponibles pour la recherche if (allWords.size === 0) { log("âš ï¸ Aucun mot à rechercher dans les lexiques actifs"); return; } + // Crée une expression régulière pour rechercher les mots const wordsPattern = Array.from(allWords) - .sort((a, b) => b.length - a.length) - .map(escapeRegExp) - .join("|"); + .sort((a, b) => b.length - a.length) // Trie les mots par longueur décroissante + .map(escapeRegExp) // Échappe les caractères spéciaux + .join("|"); // Joint les mots avec un séparateur "ou" + // Vérifie si le motif de recherche est valide if (!wordsPattern) { log("âš ï¸ Aucun mot à rechercher, sortie"); return; } - const regex = new RegExp(`\\b(${wordsPattern})\\b`, "gi"); - let match; - let matchCount = 0; + const regex = new RegExp(`\\b(${wordsPattern})\\b`, "gi"); // Crée l'expression régulière + let match; // Variable pour stocker les correspondances + let matchCount = 0; // Compteur de correspondances trouvées + // Recherche les correspondances dans le texte while ((match = regex.exec(text)) !== null) { - matchCount++; + matchCount++; // Incrémente le compteur de correspondances + // Ajoute le texte avant la correspondance au tableau de parties if (match.index > lastIndex) { fragments.push(document.createTextNode(text.slice(lastIndex, match.index))); } @@ -403,33 +479,42 @@ browser.runtime.onMessage.addListener((message) => { span.className = "lexicon-highlight"; span.style.display = "inline-block"; + // Récupère les identifiants de lexiques associés à la correspondance const matchedLexiconIds = matchedLexiconIdsMap.get(match[0].toLowerCase()) || []; span.setAttribute('data-lexicons', JSON.stringify(matchedLexiconIds)); + // Applique le style de surlignage if (matchedLexiconIds.length === 0) { span.style.backgroundColor = "rgba(255, 255, 0, 0.3)"; } else { updateHighlightStyle(span, matchedLexiconIds); } - fragments.push(span); - lastIndex = regex.lastIndex; + fragments.push(span); // Ajoute le span au tableau de parties + lastIndex = regex.lastIndex; // Met à jour l'index de la dernière correspondance } if (matchCount > 0) { log(`✨ ${matchCount} correspondances trouvées dans le nÅ“ud`); } + // Ajoute le texte restant après la dernière correspondance if (lastIndex < text.length) { fragments.push(document.createTextNode(text.slice(lastIndex))); } + // Insère les parties dans le DOM if (fragments.length > 0) { - const parent = textNode.parentNode; - fragments.forEach(fragment => parent.insertBefore(fragment, textNode)); - parent.removeChild(textNode); + const parent = textNode.parentNode; // Récupère le parent du nÅ“ud de texte + fragments.forEach(fragment => parent.insertBefore(fragment, textNode)); // Insère chaque partie avant le nÅ“ud de texte + parent.removeChild(textNode); // Supprime le nÅ“ud de texte original } } - // Suppression de tous les surlignages + // ─────────────────────────────── + // â–Œ Suppression de tous les surlignages + // ─────────────────────────────── + /** + * Supprime tous les surlignages de la page + */ function removeAllHighlights() { - log("🧹 Suppression de tous les surlignages"); + log("Suppression de tous les surlignages"); const highlights = document.querySelectorAll('.lexicon-highlight'); log(`📊 ${highlights.length} surlignages à supprimer`); highlights.forEach(highlight => { @@ -439,31 +524,41 @@ browser.runtime.onMessage.addListener((message) => { }); } - // Gestion des mutations DOM + // ─────────────────────────────── + // â–Œ Gestion des mutations DOM + // ─────────────────────────────── + /** + * Attache un observateur de mutations DOM + */ function attachMutationObserver() { - log("👀 Attachement de l'observateur de mutations"); + log("Attachement de l'observateur de mutations"); let debounceTimer = null; const DEBOUNCE_DELAY = 250; // ms observer = new MutationObserver((mutations) => { if (debounceTimer) { clearTimeout(debounceTimer); } + // Définit un nouveau timer debounceTimer = setTimeout(() => { log(`🔄 Traitement groupé de ${mutations.length} mutations DOM`); let shouldHighlight = false; for (const mutation of mutations) { + // Vérifie si la mutation concerne des nÅ“uds enfants if (mutation.type === 'childList') { + // Parcourt les nÅ“uds ajoutés for (const node of mutation.addedNodes) { + // Vérifie si le nÅ“ud est un élément et n'est pas déjà surligné if (node.nodeType === Node.ELEMENT_NODE && !node.closest('.lexicon-highlight') && node.textContent.trim()) { - shouldHighlight = true; - break; + shouldHighlight = true; // Indique qu'un surlignage est nécessaire + break; // Sort de la boucle si un nÅ“ud à surligner est trouvé } } } if (shouldHighlight) break; } + // Si un surlignage est nécessaire, appelle la fonction pour surligner le contenu visible if (shouldHighlight) { highlightVisibleContent(); } @@ -474,53 +569,97 @@ browser.runtime.onMessage.addListener((message) => { subtree: true }); } - + /** + * Détache un observateur de mutations DOM + */ function detachMutationObserver() { if (observer) { - log("👋 Détachement de l'observateur de mutations"); + log("Détachement de l'observateur de mutations"); observer.disconnect(); observer = null; } } - function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - function getLexiconIdFromName(lexiconName) { - const match = lexiconName.match(/\[(\d+)\]$/); - const id = match ? parseInt(match[1]) : null; - log(`ðŸ·ï¸ Extraction de l'ID depuis '${lexiconName}': ${id}`); - return id; - } - - log("🔠Vérification des dépendances au chargement:", { - authToken: !!window.authToken, - getAllLexiconWords: typeof window.getAllLexiconWords + // ─────────────────────────────── + // â–Œ Gestion des messages du background + // ─────────────────────────────── + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + log("Message reçu:", message, "Context:", { + highlightingActive, + activeLexiconIds: Array.from(activeLexiconIds), + hasAuthToken: !!window.authToken, + hasGetAllLexiconWords: !!window.getAllLexiconWords + }); + + // Vérifie si la commande est pour activer le surlignage + if (message.command === "activate-highlighting") { + log(`🎯 Activation du surlignage pour le lexique ${message.lexiconId}`); + // Démarre le surlignage pour le lexique spécifié + startHighlighting(message.lexiconId) + .then(() => { + window.highlightingActive = true; // Met à jour l'état du surlignage + sendResponse(true); // Envoie une réponse de succès + }) + .catch(error => { + log("Erreur lors de l'activation:", error); + sendResponse(false); + }); + return true; + } + + // Vérifie si la commande est pour désactiver le surlignage + if (message.command === "deactivate-highlighting") { + log(`🚫 Désactivation du surlignage pour le lexique ${message.lexiconId}`); + // Arrête le surlignage pour le lexique spécifié + stopHighlighting(message.lexiconId) + .then(() => { + // Vérifie si aucun lexique n'est actif + if (activeLexiconIds.size === 0) { + window.highlightingActive = false; + } + sendResponse(true); + }) + .catch(error => { + log("Erreur lors de la désactivation:", error); + sendResponse(false); + }); + return true; + } + + return false; }); + // ─────────────────────────────── + // â–Œ Restauration de l'état du surlignage au chargement + // ─────────────────────────────── + /** + * Vérifie et restaure l'état du surlignage au chargement + */ async function checkAndRestoreHighlightingState() { try { + // Récupère les lexiques actifs depuis le stockage local const { activeLexicons } = await browser.storage.local.get("activeLexicons"); + // Vérifie si des lexiques actifs ont été trouvés if (!activeLexicons || !Array.isArray(activeLexicons) || activeLexicons.length === 0) { - window.highlightingActive = false; - highlightingActive = false; - return; + window.highlightingActive = false; // Désactive le surlignage + highlightingActive = false; // Met à jour l'état local + return; // Sort de la fonction si aucun lexique actif } - log("🔄 État des lexiques trouvé:", activeLexicons); + log("État des lexiques trouvé:", activeLexicons); for (const lexiconId of activeLexicons) { - await startHighlighting(lexiconId); + await startHighlighting(lexiconId); // Démarre le surlignage pour chaque lexique } } catch (error) { - log("⌠Erreur lors de la restauration de l'état:", error); + log("Erreur lors de la restauration de l'état:", error); window.highlightingActive = false; highlightingActive = false; } } + // Démarrage initial de la restauration de l'état checkAndRestoreHighlightingState(); } catch (error) { - log("🔴 Erreur critique dans l'IIFE:", error); + log("Erreur critique dans l'IIFE:", error); } })(); diff --git a/src/utils/logger.js b/src/utils/logger.js index 3d3891c61284b17268311004cb1246ab881987da..15d163bac1c525548dc3e013ae8bb6065c9bcfdf 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,11 +1,15 @@ /** - * Mode debug : affiche les logs dans la console - * Mode prod : masque les logs ainsi que les identifiants des lexiques -*/ + * Ce script définit une fonction de log qui s'active en mode debug. + * Si la variable DEBUG est définie sur true, les messages de log seront affichés dans la console. + * Sinon, ils seront masqués. + * En mode debug, les identifiants (id) des lexiques de l'utilisateur sont affichés. + */ + (function () { + // Vérifie si le code s'exécute dans un environnement de navigateur if (typeof window !== 'undefined') { if (typeof window.DEBUG === 'undefined') { - window.DEBUG = true; // true en debug + window.DEBUG = true; // true en mode debug } if (!window.log) { function log(...args) { @@ -13,11 +17,13 @@ console.log(...args); } } - window.log = log; + window.log = log; // Assigne la fonction log à l'objet window } - } else if (typeof self !== 'undefined') { + } + // Vérifie si le code s'exécute dans un environnement worker + else if (typeof self !== 'undefined') { if (typeof self.DEBUG === 'undefined') { - self.DEBUG = true; // true en debug + self.DEBUG = true; // true en mode debug } if (!self.log) { function log(...args) { @@ -25,7 +31,7 @@ console.log(...args); } } - self.log = log; + self.log = log; // Assigne la fonction log à l'objet self } } })(); diff --git a/src/utils/stats.js b/src/utils/stats.js index 3ee90b95e7e2d386ac56625195c7b99ef98e9497..775a3054dfc1dbfa75828d9783cf194fd2f7bdf7 100644 --- a/src/utils/stats.js +++ b/src/utils/stats.js @@ -1,4 +1,6 @@ +log("Stats.js chargé."); (function () { + if (window.hasRun) { return; } @@ -119,25 +121,12 @@ if (!readContent.has(text)) { startReadingTimer(element, text, minReadTime); } - // highlightElement(element, true); } else { stopReadingTimer(element, text); - // highlightElement(element, false); } }); } - // /** - // * Ajoute une bordure rouge autour de l'élément - // */ - // function highlightElement(element, shouldHighlight) { - // if (shouldHighlight) { - // element.style.outline = "3px solid red"; - // } else { - // element.style.outline = ""; - // removeReadingIndicator(element); - // } - // } /** * Démarre un chronomètre pour vérifier si l'élément est lu @@ -217,7 +206,7 @@ */ function removeReadingIndicator(element) { if (readingTimers.has(element)) { - let { counter } = readingTimers.get(element); + let { counter } = readingTimers.get(element); if (counter) { counter.remove(); } diff --git a/src/workers/pyodide_worker.js b/src/workers/pyodide_worker.js index ebd2b0b34038f576f431bed94089a006244f84f5..40ed4646d69c708e69a480a965d460d2706b9cf6 100644 --- a/src/workers/pyodide_worker.js +++ b/src/workers/pyodide_worker.js @@ -1,14 +1,17 @@ importScripts("../utils/logger.js"); log("pyodide_worker.js chargé avec succès !"); -// URL de la version Pyodide la plus récente +// ───────────────────────────────────────────────────────────── +// â–Œ Constantes et variables globales +// ───────────────────────────────────────────────────────────── const LATEST_BASE_URL = "https://cdn.jsdelivr.net/pyodide/v0.27.2/full/"; let pyodide = null; -let pyodideLoaded = false; // Indique si Pyodide est en mémoire -let simplemmaLoaded = false; // Indique si simplemma est déjà installé +let pyodideLoaded = false; // Indique si Pyodide est chargé +let simplemmaLoaded = false; // Indique si simplemma est installé let storedFrequencies = {}; // Stockage des fréquences accumulées +// Préférences et configuration utilisateur let autoAddEnabled = false; // Ajout automatique désactivé par défaut let isAuthenticated = false; // Non connecté par défaut let userThreshold = 10; // Seuil par défaut diff --git a/stoplist_fr.txt b/stoplist_fr.txt new file mode 100644 index 0000000000000000000000000000000000000000..36fa27bf0ff4d49b4beb3c965986e4770af75be8 --- /dev/null +++ b/stoplist_fr.txt @@ -0,0 +1,116 @@ +alors +au +aucuns +aussi +autre +avant +avec +avoir +bon +car +ce +cela +ces +ceux +chaque +ci +comme +comment +dans +des +du +dedans +dehors +depuis +devrait +doit +donc +dos +début +elle +elles +en +encore +essai +est +et +eu +fait +faites +fois +font +hors +ici +il +ils +je +juste +la +le +les +leur +là +ma +maintenant +mais +mes +mien +moins +mon +mot +même +ni +nommés +notre +nous +ou +où +par +parce +pas +peut +peu +plupart +pour +pourquoi +quand +que +quel +quelle +quelles +quels +qui +sa +sans +ses +seulement +si +sien +son +sont +sous +soyez +sujet +sur +ta +tandis +tellement +tels +tes +ton +tous +tout +trop +très +tu +voient +vont +votre +vous +vu +ça +étaient +état +étions +été +être \ No newline at end of file