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 a98e394..c0bb799 100644 Binary files a/memento-note/extension/memento-web-clipper-chrome-store.zip and b/memento-note/extension/memento-web-clipper-chrome-store.zip differ diff --git a/memento-note/extension/memento-web-clipper-firefox.xpi b/memento-note/extension/memento-web-clipper-firefox.xpi index b202792..3aa302f 100644 Binary files a/memento-note/extension/memento-web-clipper-firefox.xpi and b/memento-note/extension/memento-web-clipper-firefox.xpi differ diff --git a/memento-note/extension/sidepanel.js b/memento-note/extension/sidepanel.js index e0f9511..6a21b0c 100644 --- a/memento-note/extension/sidepanel.js +++ b/memento-note/extension/sidepanel.js @@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = true 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 = `