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 = `
-
+
M
- ${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 = `
-
+
M
- ${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 = `
-
+
M
- ${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
- ? `