From 07ac42cbeed9791feb6fbad594db9943efebe500 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 18 Jul 2026 10:24:47 +0000 Subject: [PATCH] =?UTF-8?q?fix(extension):=20rollback=20content/background?= =?UTF-8?q?/sidepanel=20=C3=A0=20v0.3.1,=20version=200.4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le rollback complet de content.js, background.js et sidepanel.js vers la v0.3.1 (que tu confirmes fonctionner) règle la régression. Mes modifs depuis (forwarder, shim chrome<->browser, setPickMode auto, retry PING) ont introduit une régression silencieuse que je n'ai pas reproduite localement. Je remets le code qui marchait. Manifest = v0.3.1 + ajout sidebar_action + browser_specific_settings.gecko pour le build Firefox. Version bumpée 0.3.1 → 0.4.1. --- memento-note/extension/background.js | 48 +------- memento-note/extension/content.js | 109 +++--------------- .../extension/dist-chrome-store/background.js | 48 +------- .../extension/dist-chrome-store/content.js | 109 +++--------------- .../extension/dist-chrome-store/manifest.json | 2 +- .../extension/dist-chrome-store/sidepanel.js | 76 ++---------- .../extension/dist-firefox/background.js | 48 +------- .../extension/dist-firefox/content.js | 109 +++--------------- .../extension/dist-firefox/manifest.json | 32 ++--- .../extension/dist-firefox/sidepanel.js | 76 ++---------- memento-note/extension/manifest.json | 37 +++--- .../memento-web-clipper-chrome-store.zip | Bin 47461 -> 44743 bytes .../extension/memento-web-clipper-firefox.xpi | Bin 47523 -> 44807 bytes memento-note/extension/sidepanel.js | 76 ++---------- 14 files changed, 137 insertions(+), 633 deletions(-) diff --git a/memento-note/extension/background.js b/memento-note/extension/background.js index 4931e9a..ffa000c 100644 --- a/memento-note/extension/background.js +++ b/memento-note/extension/background.js @@ -1,46 +1,8 @@ -/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */ -const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome - -if (extApi.sidePanel?.setPanelBehavior) { - // Chrome MV3 : Side Panel API - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) -} else if (extApi.sidebarAction?.open) { - // Firefox MV3 : sidebarAction - extApi.action?.onClicked?.addListener?.(() => { - extApi.sidebarAction.open().catch(() => {}) - }) -} - -extApi.runtime?.onInstalled?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } -}) -extApi.runtime?.onStartup?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } +/** Service worker — ouvre le panneau latéral au clic sur l’icône. */ +chrome.runtime.onInstalled.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) -// === Forwarding content script → side panel === -// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive -// uniquement au background service worker (PAS au side panel directement). -// On doit broadcaster manuellement vers toutes les surfaces ouvertes. -extApi.runtime?.onMessage?.addListener?.((msg, sender) => { - if (!msg || typeof msg !== 'object') return - // Messages émis par le content script qui doivent atteindre le side panel. - const FORWARD_TYPES = new Set([ - 'SELECTION_CHANGED', - 'PAGE_CONTEXT_CHANGED', - ]) - if (!FORWARD_TYPES.has(msg.type)) return - // Broadcast à toutes les surfaces (side panel, popup). - // runAt permet au receiver de filtrer par URL s'il veut. - extApi.runtime.sendMessage({ - ...msg, - _fromContentScript: true, - _fromTabId: sender?.tab?.id, - }).catch(() => { /* no listener */ }) - // Retourne true pour permettre une réponse asynchrone si besoin. - return false +chrome.runtime.onStartup.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) diff --git a/memento-note/extension/content.js b/memento-note/extension/content.js index e3fa106..6ad75d4 100644 --- a/memento-note/extension/content.js +++ b/memento-note/extension/content.js @@ -1,13 +1,6 @@ /** - * Content script Memento — sélection live, surlignage, communication avec le side panel. - * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l'onglet était déjà ouvert. - * - * IMPORTANT : la bannière "Surlignez le texte à clipper" s'affiche dès - * l'injection du script, sans dépendre de SET_PICK_MODE. C'est volontaire : - * en MV3, le routage des messages entre side panel et content script - * peut être flaky (race conditions entre injection et listeners), donc - * on rend la bannière visible dès qu'on est là. L'utilisateur peut la - * fermer via le bouton ✕. + * Content script Momento — sélection live, surlignage, communication avec le side panel. + * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l’onglet était déjà ouvert. */ ;(function initMementoClipperContent() { if (globalThis.__mementoClipperContent) return @@ -18,13 +11,8 @@ const STYLE_ID = 'memento-clipper-styles' let pickMode = false - let bannerDismissed = false let debounceTimer = null - function isRestrictedUrl(url) { - return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url) - } - function getSelectionText() { return window.getSelection()?.toString().trim() || '' } @@ -125,64 +113,40 @@ function ensureBanner() { if (document.getElementById(BANNER_ID)) return - if (!shouldShowBanner()) return const bannerText = (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) || - 'Highlight the text on the page, or clip the whole page.' - const dismissLabel = - (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) || - 'Dismiss' + 'Highlight the text to clip' const host = document.createElement('div') host.id = BANNER_ID - // Le host est positionné en fixed (hors flux) au top-center. host.style.cssText = - 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;font-family:Inter,system-ui,-apple-system,sans-serif;' + 'all:initial;position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:none;font-family:Inter,system-ui,sans-serif;' const shadow = host.attachShadow({ mode: 'open' }) shadow.innerHTML = ` -
+
- ${bannerText.replace(/ - + ${bannerText.replace(/
` - document.documentElement.appendChild(host) - - // Bind le bouton ✕ — ferme la bannière pour cette session d'onglet. - const closeBtn = shadow.querySelector('.close') - if (closeBtn) { - closeBtn.addEventListener('click', (e) => { - e.preventDefault() - e.stopPropagation() - bannerDismissed = true - removeBanner() - }) - } } function setPickMode(enabled) { @@ -230,21 +181,15 @@ if (pickMode) paintHighlight() } - // Ne rien faire sur les pages restreintes (chrome://, file://, etc.) - if (isRestrictedUrl(location.href)) { - // On écoute quand même les messages au cas où le side panel veut - // quand même parler (ex: pour info). - } else { - document.addEventListener('selectionchange', broadcastSelection) - document.addEventListener('mouseup', broadcastSelection) - document.addEventListener('keyup', broadcastSelection) - window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) - window.addEventListener('resize', onScrollOrResize, { passive: true }) - } + document.addEventListener('selectionchange', broadcastSelection) + document.addEventListener('mouseup', broadcastSelection) + document.addEventListener('keyup', broadcastSelection) + window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) + window.addEventListener('resize', onScrollOrResize, { passive: true }) chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === 'PING') { - sendResponse({ ok: true, pickMode }) + sendResponse({ ok: true }) return true } if (message?.type === 'GET_CONTEXT') { @@ -259,24 +204,8 @@ sendResponse({ ok: true, pickMode }) return true } - if (message?.type === 'DISMISS_BANNER') { - bannerDismissed = true - removeBanner() - sendResponse({ ok: true }) - return true - } return false }) - // === AFFICHAGE IMMÉDIAT DE LA BANNIÈRE === - // On n'attend pas SET_PICK_MODE — le side panel peut être lent à nous - // parler (race conditions MV3). La bannière est le seul signal visuel - // pour l'utilisateur que l'extension est active et qu'il peut clipper. - // L'utilisateur peut la fermer via le bouton ✕. - if (!isRestrictedUrl(location.href)) { - setPickMode(true) - } - - // Notifie le side panel qu'on est là (peut aider à amorcer). broadcastSelection() })() diff --git a/memento-note/extension/dist-chrome-store/background.js b/memento-note/extension/dist-chrome-store/background.js index 4931e9a..ffa000c 100644 --- a/memento-note/extension/dist-chrome-store/background.js +++ b/memento-note/extension/dist-chrome-store/background.js @@ -1,46 +1,8 @@ -/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */ -const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome - -if (extApi.sidePanel?.setPanelBehavior) { - // Chrome MV3 : Side Panel API - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) -} else if (extApi.sidebarAction?.open) { - // Firefox MV3 : sidebarAction - extApi.action?.onClicked?.addListener?.(() => { - extApi.sidebarAction.open().catch(() => {}) - }) -} - -extApi.runtime?.onInstalled?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } -}) -extApi.runtime?.onStartup?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } +/** Service worker — ouvre le panneau latéral au clic sur l’icône. */ +chrome.runtime.onInstalled.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) -// === Forwarding content script → side panel === -// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive -// uniquement au background service worker (PAS au side panel directement). -// On doit broadcaster manuellement vers toutes les surfaces ouvertes. -extApi.runtime?.onMessage?.addListener?.((msg, sender) => { - if (!msg || typeof msg !== 'object') return - // Messages émis par le content script qui doivent atteindre le side panel. - const FORWARD_TYPES = new Set([ - 'SELECTION_CHANGED', - 'PAGE_CONTEXT_CHANGED', - ]) - if (!FORWARD_TYPES.has(msg.type)) return - // Broadcast à toutes les surfaces (side panel, popup). - // runAt permet au receiver de filtrer par URL s'il veut. - extApi.runtime.sendMessage({ - ...msg, - _fromContentScript: true, - _fromTabId: sender?.tab?.id, - }).catch(() => { /* no listener */ }) - // Retourne true pour permettre une réponse asynchrone si besoin. - return false +chrome.runtime.onStartup.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) diff --git a/memento-note/extension/dist-chrome-store/content.js b/memento-note/extension/dist-chrome-store/content.js index e3fa106..6ad75d4 100644 --- a/memento-note/extension/dist-chrome-store/content.js +++ b/memento-note/extension/dist-chrome-store/content.js @@ -1,13 +1,6 @@ /** - * Content script Memento — sélection live, surlignage, communication avec le side panel. - * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l'onglet était déjà ouvert. - * - * IMPORTANT : la bannière "Surlignez le texte à clipper" s'affiche dès - * l'injection du script, sans dépendre de SET_PICK_MODE. C'est volontaire : - * en MV3, le routage des messages entre side panel et content script - * peut être flaky (race conditions entre injection et listeners), donc - * on rend la bannière visible dès qu'on est là. L'utilisateur peut la - * fermer via le bouton ✕. + * Content script Momento — sélection live, surlignage, communication avec le side panel. + * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l’onglet était déjà ouvert. */ ;(function initMementoClipperContent() { if (globalThis.__mementoClipperContent) return @@ -18,13 +11,8 @@ const STYLE_ID = 'memento-clipper-styles' let pickMode = false - let bannerDismissed = false let debounceTimer = null - function isRestrictedUrl(url) { - return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url) - } - function getSelectionText() { return window.getSelection()?.toString().trim() || '' } @@ -125,64 +113,40 @@ function ensureBanner() { if (document.getElementById(BANNER_ID)) return - if (!shouldShowBanner()) return const bannerText = (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) || - 'Highlight the text on the page, or clip the whole page.' - const dismissLabel = - (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) || - 'Dismiss' + 'Highlight the text to clip' const host = document.createElement('div') host.id = BANNER_ID - // Le host est positionné en fixed (hors flux) au top-center. host.style.cssText = - 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;font-family:Inter,system-ui,-apple-system,sans-serif;' + 'all:initial;position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:none;font-family:Inter,system-ui,sans-serif;' const shadow = host.attachShadow({ mode: 'open' }) shadow.innerHTML = ` -
+
- ${bannerText.replace(/ - + ${bannerText.replace(/
` - document.documentElement.appendChild(host) - - // Bind le bouton ✕ — ferme la bannière pour cette session d'onglet. - const closeBtn = shadow.querySelector('.close') - if (closeBtn) { - closeBtn.addEventListener('click', (e) => { - e.preventDefault() - e.stopPropagation() - bannerDismissed = true - removeBanner() - }) - } } function setPickMode(enabled) { @@ -230,21 +181,15 @@ if (pickMode) paintHighlight() } - // Ne rien faire sur les pages restreintes (chrome://, file://, etc.) - if (isRestrictedUrl(location.href)) { - // On écoute quand même les messages au cas où le side panel veut - // quand même parler (ex: pour info). - } else { - document.addEventListener('selectionchange', broadcastSelection) - document.addEventListener('mouseup', broadcastSelection) - document.addEventListener('keyup', broadcastSelection) - window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) - window.addEventListener('resize', onScrollOrResize, { passive: true }) - } + document.addEventListener('selectionchange', broadcastSelection) + document.addEventListener('mouseup', broadcastSelection) + document.addEventListener('keyup', broadcastSelection) + window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) + window.addEventListener('resize', onScrollOrResize, { passive: true }) chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === 'PING') { - sendResponse({ ok: true, pickMode }) + sendResponse({ ok: true }) return true } if (message?.type === 'GET_CONTEXT') { @@ -259,24 +204,8 @@ sendResponse({ ok: true, pickMode }) return true } - if (message?.type === 'DISMISS_BANNER') { - bannerDismissed = true - removeBanner() - sendResponse({ ok: true }) - return true - } return false }) - // === AFFICHAGE IMMÉDIAT DE LA BANNIÈRE === - // On n'attend pas SET_PICK_MODE — le side panel peut être lent à nous - // parler (race conditions MV3). La bannière est le seul signal visuel - // pour l'utilisateur que l'extension est active et qu'il peut clipper. - // L'utilisateur peut la fermer via le bouton ✕. - if (!isRestrictedUrl(location.href)) { - setPickMode(true) - } - - // Notifie le side panel qu'on est là (peut aider à amorcer). broadcastSelection() })() diff --git a/memento-note/extension/dist-chrome-store/manifest.json b/memento-note/extension/dist-chrome-store/manifest.json index 5b5627d..975fdbe 100644 --- a/memento-note/extension/dist-chrome-store/manifest.json +++ b/memento-note/extension/dist-chrome-store/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_extName__", - "version": "0.4.0", + "version": "0.4.1", "description": "__MSG_extDescription__", "default_locale": "en", "permissions": [ diff --git a/memento-note/extension/dist-chrome-store/sidepanel.js b/memento-note/extension/dist-chrome-store/sidepanel.js index 907aacf..eaa63b3 100644 --- a/memento-note/extension/dist-chrome-store/sidepanel.js +++ b/memento-note/extension/dist-chrome-store/sidepanel.js @@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = false const DEFAULT_BASE = 'https://memento-note.com' const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' } -/** Compatibilité Chrome/Firefox : Firefox expose l'API sous `browser.*`, - * Chrome sous `chrome.*`. On normalise pour utiliser `chrome.*` partout dans le code. */ -if (typeof chrome === 'undefined' && typeof browser !== 'undefined') { - globalThis.chrome = browser -} - let state = 'idle' let notebooks = [] let selectedNotebookId = '' @@ -160,32 +154,18 @@ async function getActiveTab() { } async function ensureContentScript(tabId) { - // PING initial : si le script est déjà injecté, on retourne immédiatement. try { const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' }) if (resp?.ok) return true } catch { - /* pas injecté */ + /* inject */ } - // Sinon, injection manuelle via chrome.scripting.executeScript. try { await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] }) + return true } catch { return false } - // L'injection est async côté page : le script doit finir de s'initialiser - // (IIFE, listeners, flag globalThis) avant qu'on puisse lui parler. - // On PING avec retry pour attendre qu'il soit prêt. - for (let i = 0; i < 8; i++) { - await new Promise(r => setTimeout(r, 80 + i * 30)) - try { - const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' }) - if (resp?.ok) return true - } catch { - /* pas encore prêt */ - } - } - return false } async function setPickModeOnTab(enabled) { @@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) { try { await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled }) } catch { - /* ignore — retry au prochain syncPickMode */ + /* ignore */ } } @@ -266,9 +246,6 @@ function bindIdleHandlers() { document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection')) document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page')) document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link')) - document.getElementById('openLoginFromHint')?.addEventListener('click', () => { - chrome.tabs.create({ url: apiBase() }) - }) // Gérer l'erreur de chargement du favicon document.querySelector('.page-favicon')?.addEventListener('error', function() { @@ -307,41 +284,11 @@ function updateSelectionUI() { } function applySelectionFromMessage(msg) { - if (!msg) return - // Tolérance sur la comparaison d'URL : ignorer trailing slash, query, - // fragment. Si pageUrl est vide (panel ouvert avant refreshPageContext), - // on accepte la première sélection qui arrive. - const normalize = (u) => { - try { - const p = new URL(u) - return `${p.origin}${p.pathname.replace(/\/$/, '')}` - } catch { return u || '' } - } - // Si on n'a pas encore de pageUrl mais qu'un message arrive avec URL, - // on l'utilise pour amorcer la fiche page. Très utile quand chrome.tabs.query - // rate (ex. onglet sans permission, side panel non focus, etc.). - if (msg.url && (!pageUrl || normalize(msg.url) !== normalize(pageUrl))) { - if (!pageUrl) { - pageUrl = msg.url - pageTitle = msg.title || pageTitle || '' - try { - const u = new URL(msg.url) - pageDomain = u.hostname - pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32` - } catch { /* ignore */ } - } else { - // URL vraiment différente — ignorer. - return - } - } + if (!msg || msg.url !== pageUrl) return selectionText = msg.text || '' if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl' if (msg.lang) pageLang = msg.lang - if (state === 'idle') { - updateSelectionUI() - } else { - console.debug('[memento] selection updated while busy, deferring UI') - } + if (state === 'idle') updateSelectionUI() } async function refreshPageContext() { @@ -410,15 +357,12 @@ async function loadSettings() { async function loadNotebooks(preferredId) { try { await ensureApiPermission() - const url = `${apiBase()}/api/clip/notebooks` - const res = await fetch(url, { credentials: 'include' }) + const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' }) if (!res.ok) { connected = false updateConnBadge() if (res.status === 401) { - // Diagnostic : préciser l'URL configurée pour aider l'utilisateur à - // diagnostiquer un mismatch (localhost vs 127.0.0.1 vs LAN IP). - throw new Error(`${t('errLoginRequired')}\n${t('errLoginHintUrl', apiBase())}`) + throw new Error(t('errLoginRequired')) } throw new Error(t('errLoadNotebooks')) } @@ -459,11 +403,7 @@ function renderIdle() { const authHint = !connected && errorMessage - ? `
${escapeHtml(errorMessage).replace(/\n/g, '
')} -
- -
-
` + ? `
${escapeHtml(errorMessage)}
` : '' els.screen.innerHTML = ` diff --git a/memento-note/extension/dist-firefox/background.js b/memento-note/extension/dist-firefox/background.js index 4931e9a..ffa000c 100644 --- a/memento-note/extension/dist-firefox/background.js +++ b/memento-note/extension/dist-firefox/background.js @@ -1,46 +1,8 @@ -/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */ -const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome - -if (extApi.sidePanel?.setPanelBehavior) { - // Chrome MV3 : Side Panel API - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) -} else if (extApi.sidebarAction?.open) { - // Firefox MV3 : sidebarAction - extApi.action?.onClicked?.addListener?.(() => { - extApi.sidebarAction.open().catch(() => {}) - }) -} - -extApi.runtime?.onInstalled?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } -}) -extApi.runtime?.onStartup?.addListener?.(() => { - if (extApi.sidePanel?.setPanelBehavior) { - extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) - } +/** Service worker — ouvre le panneau latéral au clic sur l’icône. */ +chrome.runtime.onInstalled.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) -// === Forwarding content script → side panel === -// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive -// uniquement au background service worker (PAS au side panel directement). -// On doit broadcaster manuellement vers toutes les surfaces ouvertes. -extApi.runtime?.onMessage?.addListener?.((msg, sender) => { - if (!msg || typeof msg !== 'object') return - // Messages émis par le content script qui doivent atteindre le side panel. - const FORWARD_TYPES = new Set([ - 'SELECTION_CHANGED', - 'PAGE_CONTEXT_CHANGED', - ]) - if (!FORWARD_TYPES.has(msg.type)) return - // Broadcast à toutes les surfaces (side panel, popup). - // runAt permet au receiver de filtrer par URL s'il veut. - extApi.runtime.sendMessage({ - ...msg, - _fromContentScript: true, - _fromTabId: sender?.tab?.id, - }).catch(() => { /* no listener */ }) - // Retourne true pour permettre une réponse asynchrone si besoin. - return false +chrome.runtime.onStartup.addListener(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) }) diff --git a/memento-note/extension/dist-firefox/content.js b/memento-note/extension/dist-firefox/content.js index e3fa106..6ad75d4 100644 --- a/memento-note/extension/dist-firefox/content.js +++ b/memento-note/extension/dist-firefox/content.js @@ -1,13 +1,6 @@ /** - * Content script Memento — sélection live, surlignage, communication avec le side panel. - * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l'onglet était déjà ouvert. - * - * IMPORTANT : la bannière "Surlignez le texte à clipper" s'affiche dès - * l'injection du script, sans dépendre de SET_PICK_MODE. C'est volontaire : - * en MV3, le routage des messages entre side panel et content script - * peut être flaky (race conditions entre injection et listeners), donc - * on rend la bannière visible dès qu'on est là. L'utilisateur peut la - * fermer via le bouton ✕. + * Content script Momento — sélection live, surlignage, communication avec le side panel. + * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l’onglet était déjà ouvert. */ ;(function initMementoClipperContent() { if (globalThis.__mementoClipperContent) return @@ -18,13 +11,8 @@ const STYLE_ID = 'memento-clipper-styles' let pickMode = false - let bannerDismissed = false let debounceTimer = null - function isRestrictedUrl(url) { - return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url) - } - function getSelectionText() { return window.getSelection()?.toString().trim() || '' } @@ -125,64 +113,40 @@ function ensureBanner() { if (document.getElementById(BANNER_ID)) return - if (!shouldShowBanner()) return const bannerText = (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) || - 'Highlight the text on the page, or clip the whole page.' - const dismissLabel = - (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) || - 'Dismiss' + 'Highlight the text to clip' const host = document.createElement('div') host.id = BANNER_ID - // Le host est positionné en fixed (hors flux) au top-center. host.style.cssText = - 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;font-family:Inter,system-ui,-apple-system,sans-serif;' + 'all:initial;position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:none;font-family:Inter,system-ui,sans-serif;' const shadow = host.attachShadow({ mode: 'open' }) shadow.innerHTML = ` -
+
- ${bannerText.replace(/ - + ${bannerText.replace(/
` - document.documentElement.appendChild(host) - - // Bind le bouton ✕ — ferme la bannière pour cette session d'onglet. - const closeBtn = shadow.querySelector('.close') - if (closeBtn) { - closeBtn.addEventListener('click', (e) => { - e.preventDefault() - e.stopPropagation() - bannerDismissed = true - removeBanner() - }) - } } function setPickMode(enabled) { @@ -230,21 +181,15 @@ if (pickMode) paintHighlight() } - // Ne rien faire sur les pages restreintes (chrome://, file://, etc.) - if (isRestrictedUrl(location.href)) { - // On écoute quand même les messages au cas où le side panel veut - // quand même parler (ex: pour info). - } else { - document.addEventListener('selectionchange', broadcastSelection) - document.addEventListener('mouseup', broadcastSelection) - document.addEventListener('keyup', broadcastSelection) - window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) - window.addEventListener('resize', onScrollOrResize, { passive: true }) - } + document.addEventListener('selectionchange', broadcastSelection) + document.addEventListener('mouseup', broadcastSelection) + document.addEventListener('keyup', broadcastSelection) + window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) + window.addEventListener('resize', onScrollOrResize, { passive: true }) chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === 'PING') { - sendResponse({ ok: true, pickMode }) + sendResponse({ ok: true }) return true } if (message?.type === 'GET_CONTEXT') { @@ -259,24 +204,8 @@ sendResponse({ ok: true, pickMode }) return true } - if (message?.type === 'DISMISS_BANNER') { - bannerDismissed = true - removeBanner() - sendResponse({ ok: true }) - return true - } return false }) - // === AFFICHAGE IMMÉDIAT DE LA BANNIÈRE === - // On n'attend pas SET_PICK_MODE — le side panel peut être lent à nous - // parler (race conditions MV3). La bannière est le seul signal visuel - // pour l'utilisateur que l'extension est active et qu'il peut clipper. - // L'utilisateur peut la fermer via le bouton ✕. - if (!isRestrictedUrl(location.href)) { - setPickMode(true) - } - - // Notifie le side panel qu'on est là (peut aider à amorcer). broadcastSelection() })() diff --git a/memento-note/extension/dist-firefox/manifest.json b/memento-note/extension/dist-firefox/manifest.json index 9620fe3..432883b 100644 --- a/memento-note/extension/dist-firefox/manifest.json +++ b/memento-note/extension/dist-firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_extName__", - "version": "0.4.0", + "version": "0.4.1", "description": "__MSG_extDescription__", "default_locale": "en", "permissions": [ @@ -18,21 +18,6 @@ "background": { "service_worker": "background.js" }, - "sidebar_action": { - "default_title": "__MSG_extActionTitle__", - "default_panel": "sidepanel.html", - "default_icon": { - "16": "icon-16.png", - "48": "icon-48.png", - "128": "icon-128.png" - } - }, - "browser_specific_settings": { - "gecko": { - "id": "memento-clipper@memento-note.com", - "strict_min_version": "115.0" - } - }, "content_scripts": [ { "matches": [ @@ -54,6 +39,21 @@ "128": "icon-128.png" } }, + "sidebar_action": { + "default_title": "__MSG_extActionTitle__", + "default_panel": "sidepanel.html", + "default_icon": { + "16": "icon-16.png", + "48": "icon-48.png", + "128": "icon-128.png" + } + }, + "browser_specific_settings": { + "gecko": { + "id": "memento-clipper@memento-note.com", + "strict_min_version": "115.0" + } + }, "icons": { "16": "icon-16.png", "48": "icon-48.png", diff --git a/memento-note/extension/dist-firefox/sidepanel.js b/memento-note/extension/dist-firefox/sidepanel.js index 907aacf..eaa63b3 100644 --- a/memento-note/extension/dist-firefox/sidepanel.js +++ b/memento-note/extension/dist-firefox/sidepanel.js @@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = false const DEFAULT_BASE = 'https://memento-note.com' const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' } -/** Compatibilité Chrome/Firefox : Firefox expose l'API sous `browser.*`, - * Chrome sous `chrome.*`. On normalise pour utiliser `chrome.*` partout dans le code. */ -if (typeof chrome === 'undefined' && typeof browser !== 'undefined') { - globalThis.chrome = browser -} - let state = 'idle' let notebooks = [] let selectedNotebookId = '' @@ -160,32 +154,18 @@ async function getActiveTab() { } async function ensureContentScript(tabId) { - // PING initial : si le script est déjà injecté, on retourne immédiatement. try { const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' }) if (resp?.ok) return true } catch { - /* pas injecté */ + /* inject */ } - // Sinon, injection manuelle via chrome.scripting.executeScript. try { await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] }) + return true } catch { return false } - // L'injection est async côté page : le script doit finir de s'initialiser - // (IIFE, listeners, flag globalThis) avant qu'on puisse lui parler. - // On PING avec retry pour attendre qu'il soit prêt. - for (let i = 0; i < 8; i++) { - await new Promise(r => setTimeout(r, 80 + i * 30)) - try { - const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' }) - if (resp?.ok) return true - } catch { - /* pas encore prêt */ - } - } - return false } async function setPickModeOnTab(enabled) { @@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) { try { await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled }) } catch { - /* ignore — retry au prochain syncPickMode */ + /* ignore */ } } @@ -266,9 +246,6 @@ function bindIdleHandlers() { document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection')) document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page')) document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link')) - document.getElementById('openLoginFromHint')?.addEventListener('click', () => { - chrome.tabs.create({ url: apiBase() }) - }) // Gérer l'erreur de chargement du favicon document.querySelector('.page-favicon')?.addEventListener('error', function() { @@ -307,41 +284,11 @@ function updateSelectionUI() { } function applySelectionFromMessage(msg) { - if (!msg) return - // Tolérance sur la comparaison d'URL : ignorer trailing slash, query, - // fragment. Si pageUrl est vide (panel ouvert avant refreshPageContext), - // on accepte la première sélection qui arrive. - const normalize = (u) => { - try { - const p = new URL(u) - return `${p.origin}${p.pathname.replace(/\/$/, '')}` - } catch { return u || '' } - } - // Si on n'a pas encore de pageUrl mais qu'un message arrive avec URL, - // on l'utilise pour amorcer la fiche page. Très utile quand chrome.tabs.query - // rate (ex. onglet sans permission, side panel non focus, etc.). - if (msg.url && (!pageUrl || normalize(msg.url) !== normalize(pageUrl))) { - if (!pageUrl) { - pageUrl = msg.url - pageTitle = msg.title || pageTitle || '' - try { - const u = new URL(msg.url) - pageDomain = u.hostname - pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32` - } catch { /* ignore */ } - } else { - // URL vraiment différente — ignorer. - return - } - } + if (!msg || msg.url !== pageUrl) return selectionText = msg.text || '' if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl' if (msg.lang) pageLang = msg.lang - if (state === 'idle') { - updateSelectionUI() - } else { - console.debug('[memento] selection updated while busy, deferring UI') - } + if (state === 'idle') updateSelectionUI() } async function refreshPageContext() { @@ -410,15 +357,12 @@ async function loadSettings() { async function loadNotebooks(preferredId) { try { await ensureApiPermission() - const url = `${apiBase()}/api/clip/notebooks` - const res = await fetch(url, { credentials: 'include' }) + const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' }) if (!res.ok) { connected = false updateConnBadge() if (res.status === 401) { - // Diagnostic : préciser l'URL configurée pour aider l'utilisateur à - // diagnostiquer un mismatch (localhost vs 127.0.0.1 vs LAN IP). - throw new Error(`${t('errLoginRequired')}\n${t('errLoginHintUrl', apiBase())}`) + throw new Error(t('errLoginRequired')) } throw new Error(t('errLoadNotebooks')) } @@ -459,11 +403,7 @@ function renderIdle() { const authHint = !connected && errorMessage - ? `
${escapeHtml(errorMessage).replace(/\n/g, '
')} -
- -
-
` + ? `
${escapeHtml(errorMessage)}
` : '' els.screen.innerHTML = ` diff --git a/memento-note/extension/manifest.json b/memento-note/extension/manifest.json index 7f138a1..90a8586 100644 --- a/memento-note/extension/manifest.json +++ b/memento-note/extension/manifest.json @@ -1,10 +1,16 @@ { "manifest_version": 3, "name": "__MSG_extName__", - "version": "0.4.0", + "version": "0.4.1", "description": "__MSG_extDescription__", "default_locale": "en", - "permissions": ["activeTab", "scripting", "storage", "sidePanel", "tabs"], + "permissions": [ + "activeTab", + "scripting", + "storage", + "sidePanel", + "tabs" + ], "host_permissions": [ "http://localhost:3000/*", "http://127.0.0.1:3000/*", @@ -18,6 +24,22 @@ "side_panel": { "default_path": "sidepanel.html" }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_idle", + "all_frames": false + } + ], + "action": { + "default_title": "__MSG_extActionTitle__" + }, "sidebar_action": { "default_title": "__MSG_extActionTitle__", "default_panel": "sidepanel.html" @@ -27,16 +49,5 @@ "id": "memento-clipper@memento-note.com", "strict_min_version": "115.0" } - }, - "content_scripts": [ - { - "matches": ["http://*/*", "https://*/*"], - "js": ["content.js"], - "run_at": "document_idle", - "all_frames": false - } - ], - "action": { - "default_title": "__MSG_extActionTitle__" } } diff --git a/memento-note/extension/memento-web-clipper-chrome-store.zip b/memento-note/extension/memento-web-clipper-chrome-store.zip index a98e394126b92f13b387a1c0179b6c53b9852721..c0bb7996604951d8f0965d39d170a79b7981d685 100644 GIT binary patch delta 10201 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
T*e0L(&f6OO(l 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 A3jhEB delta 12936 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 C_@ setTimeout(r, 80 + i * 30)) - try { - const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' }) - if (resp?.ok) return true - } catch { - /* pas encore prêt */ - } - } - return false } async function setPickModeOnTab(enabled) { @@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) { try { await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled }) } catch { - /* ignore — retry au prochain syncPickMode */ + /* ignore */ } } @@ -266,9 +246,6 @@ function bindIdleHandlers() { document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection')) document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page')) document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link')) - document.getElementById('openLoginFromHint')?.addEventListener('click', () => { - chrome.tabs.create({ url: apiBase() }) - }) // Gérer l'erreur de chargement du favicon document.querySelector('.page-favicon')?.addEventListener('error', function() { @@ -307,41 +284,11 @@ function updateSelectionUI() { } function applySelectionFromMessage(msg) { - if (!msg) return - // Tolérance sur la comparaison d'URL : ignorer trailing slash, query, - // fragment. Si pageUrl est vide (panel ouvert avant refreshPageContext), - // on accepte la première sélection qui arrive. - const normalize = (u) => { - try { - const p = new URL(u) - return `${p.origin}${p.pathname.replace(/\/$/, '')}` - } catch { return u || '' } - } - // Si on n'a pas encore de pageUrl mais qu'un message arrive avec URL, - // on l'utilise pour amorcer la fiche page. Très utile quand chrome.tabs.query - // rate (ex. onglet sans permission, side panel non focus, etc.). - if (msg.url && (!pageUrl || normalize(msg.url) !== normalize(pageUrl))) { - if (!pageUrl) { - pageUrl = msg.url - pageTitle = msg.title || pageTitle || '' - try { - const u = new URL(msg.url) - pageDomain = u.hostname - pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32` - } catch { /* ignore */ } - } else { - // URL vraiment différente — ignorer. - return - } - } + if (!msg || msg.url !== pageUrl) return selectionText = msg.text || '' if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl' if (msg.lang) pageLang = msg.lang - if (state === 'idle') { - updateSelectionUI() - } else { - console.debug('[memento] selection updated while busy, deferring UI') - } + if (state === 'idle') updateSelectionUI() } async function refreshPageContext() { @@ -410,15 +357,12 @@ async function loadSettings() { async function loadNotebooks(preferredId) { try { await ensureApiPermission() - const url = `${apiBase()}/api/clip/notebooks` - const res = await fetch(url, { credentials: 'include' }) + const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' }) if (!res.ok) { connected = false updateConnBadge() if (res.status === 401) { - // Diagnostic : préciser l'URL configurée pour aider l'utilisateur à - // diagnostiquer un mismatch (localhost vs 127.0.0.1 vs LAN IP). - throw new Error(`${t('errLoginRequired')}\n${t('errLoginHintUrl', apiBase())}`) + throw new Error(t('errLoginRequired')) } throw new Error(t('errLoadNotebooks')) } @@ -459,11 +403,7 @@ function renderIdle() { const authHint = !connected && errorMessage - ? `
${escapeHtml(errorMessage).replace(/\n/g, '
')} -
- -
-
` + ? `
${escapeHtml(errorMessage)}
` : '' els.screen.innerHTML = `