fix(extension): rollback content/background/sidepanel à v0.3.1, version 0.4.1
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.
This commit is contained in:
@@ -1,46 +1,8 @@
|
|||||||
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
|
/** Service worker — ouvre le panneau latéral au clic sur l’icône. */
|
||||||
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
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(() => {})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// === Forwarding content script → side panel ===
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
// 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
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Content script Memento — sélection live, surlignage, communication avec le side panel.
|
* 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.
|
* 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 ✕.
|
|
||||||
*/
|
*/
|
||||||
;(function initMementoClipperContent() {
|
;(function initMementoClipperContent() {
|
||||||
if (globalThis.__mementoClipperContent) return
|
if (globalThis.__mementoClipperContent) return
|
||||||
@@ -18,13 +11,8 @@
|
|||||||
const STYLE_ID = 'memento-clipper-styles'
|
const STYLE_ID = 'memento-clipper-styles'
|
||||||
|
|
||||||
let pickMode = false
|
let pickMode = false
|
||||||
let bannerDismissed = false
|
|
||||||
let debounceTimer = null
|
let debounceTimer = null
|
||||||
|
|
||||||
function isRestrictedUrl(url) {
|
|
||||||
return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectionText() {
|
function getSelectionText() {
|
||||||
return window.getSelection()?.toString().trim() || ''
|
return window.getSelection()?.toString().trim() || ''
|
||||||
}
|
}
|
||||||
@@ -125,64 +113,40 @@
|
|||||||
|
|
||||||
function ensureBanner() {
|
function ensureBanner() {
|
||||||
if (document.getElementById(BANNER_ID)) return
|
if (document.getElementById(BANNER_ID)) return
|
||||||
if (!shouldShowBanner()) return
|
|
||||||
|
|
||||||
const bannerText =
|
const bannerText =
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
||||||
'Highlight the text on the page, or clip the whole page.'
|
'Highlight the text to clip'
|
||||||
const dismissLabel =
|
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) ||
|
|
||||||
'Dismiss'
|
|
||||||
|
|
||||||
const host = document.createElement('div')
|
const host = document.createElement('div')
|
||||||
host.id = BANNER_ID
|
host.id = BANNER_ID
|
||||||
// Le host est positionné en fixed (hors flux) au top-center.
|
|
||||||
host.style.cssText =
|
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' })
|
const shadow = host.attachShadow({ mode: 'open' })
|
||||||
shadow.innerHTML = `
|
shadow.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host { all: initial; }
|
.pill {
|
||||||
.wrap {
|
display: flex; align-items: center; gap: 10px;
|
||||||
display: inline-flex; align-items: center; gap: 10px;
|
padding: 10px 18px; border-radius: 999px;
|
||||||
padding: 10px 10px 10px 18px; border-radius: 999px;
|
|
||||||
background: #1c1c1c; color: #faf9f5;
|
background: #1c1c1c; color: #faf9f5;
|
||||||
box-shadow: 0 12px 32px rgba(0,0,0,0.32);
|
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
|
||||||
font-size: 12px; font-weight: 600;
|
font-size: 12px; font-weight: 600;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.02em;
|
||||||
max-width: min(640px, calc(100vw - 24px));
|
|
||||||
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
width: 22px; height: 22px; border-radius: 7px;
|
width: 22px; height: 22px; border-radius: 7px;
|
||||||
background: #faf9f5; color: #1c1c1c;
|
background: #faf9f5; color: #1c1c1c;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
font-family: Georgia, serif; font-weight: 900; font-size: 13px;
|
font-family: Georgia, serif; font-weight: 900; font-size: 12px;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
width: 8px; height: 8px; border-radius: 50%;
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
background: #a47148; animation: pulse 1.2s ease infinite;
|
background: #a47148; animation: pulse 1.2s ease infinite;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.close {
|
|
||||||
width: 22px; height: 22px; border-radius: 50%;
|
|
||||||
background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7);
|
|
||||||
border: none; cursor: pointer; display: flex;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
font-size: 14px; line-height: 1; font-weight: 700;
|
|
||||||
flex-shrink: 0; transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.close:hover {
|
|
||||||
background: rgba(255,255,255,0.18); color: #fff;
|
|
||||||
}
|
}
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -190,26 +154,13 @@
|
|||||||
50% { opacity: 0.5; transform: scale(0.85); }
|
50% { opacity: 0.5; transform: scale(0.85); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="wrap" role="status" aria-live="polite">
|
<div class="pill">
|
||||||
<span class="logo">M</span>
|
<span class="logo">M</span>
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="text">${bannerText.replace(/</g, '<')}</span>
|
<span>${bannerText.replace(/</g, '<')}</span>
|
||||||
<button type="button" class="close" aria-label="${dismissLabel}">×</button>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
document.documentElement.appendChild(host)
|
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) {
|
function setPickMode(enabled) {
|
||||||
@@ -230,21 +181,15 @@
|
|||||||
if (pickMode) paintHighlight()
|
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('selectionchange', broadcastSelection)
|
||||||
document.addEventListener('mouseup', broadcastSelection)
|
document.addEventListener('mouseup', broadcastSelection)
|
||||||
document.addEventListener('keyup', broadcastSelection)
|
document.addEventListener('keyup', broadcastSelection)
|
||||||
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
||||||
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
if (message?.type === 'PING') {
|
if (message?.type === 'PING') {
|
||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'GET_CONTEXT') {
|
if (message?.type === 'GET_CONTEXT') {
|
||||||
@@ -259,24 +204,8 @@
|
|||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true, pickMode })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'DISMISS_BANNER') {
|
|
||||||
bannerDismissed = true
|
|
||||||
removeBanner()
|
|
||||||
sendResponse({ ok: true })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
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()
|
broadcastSelection()
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -1,46 +1,8 @@
|
|||||||
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
|
/** Service worker — ouvre le panneau latéral au clic sur l’icône. */
|
||||||
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
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(() => {})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// === Forwarding content script → side panel ===
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
// 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
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Content script Memento — sélection live, surlignage, communication avec le side panel.
|
* 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.
|
* 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 ✕.
|
|
||||||
*/
|
*/
|
||||||
;(function initMementoClipperContent() {
|
;(function initMementoClipperContent() {
|
||||||
if (globalThis.__mementoClipperContent) return
|
if (globalThis.__mementoClipperContent) return
|
||||||
@@ -18,13 +11,8 @@
|
|||||||
const STYLE_ID = 'memento-clipper-styles'
|
const STYLE_ID = 'memento-clipper-styles'
|
||||||
|
|
||||||
let pickMode = false
|
let pickMode = false
|
||||||
let bannerDismissed = false
|
|
||||||
let debounceTimer = null
|
let debounceTimer = null
|
||||||
|
|
||||||
function isRestrictedUrl(url) {
|
|
||||||
return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectionText() {
|
function getSelectionText() {
|
||||||
return window.getSelection()?.toString().trim() || ''
|
return window.getSelection()?.toString().trim() || ''
|
||||||
}
|
}
|
||||||
@@ -125,64 +113,40 @@
|
|||||||
|
|
||||||
function ensureBanner() {
|
function ensureBanner() {
|
||||||
if (document.getElementById(BANNER_ID)) return
|
if (document.getElementById(BANNER_ID)) return
|
||||||
if (!shouldShowBanner()) return
|
|
||||||
|
|
||||||
const bannerText =
|
const bannerText =
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
||||||
'Highlight the text on the page, or clip the whole page.'
|
'Highlight the text to clip'
|
||||||
const dismissLabel =
|
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) ||
|
|
||||||
'Dismiss'
|
|
||||||
|
|
||||||
const host = document.createElement('div')
|
const host = document.createElement('div')
|
||||||
host.id = BANNER_ID
|
host.id = BANNER_ID
|
||||||
// Le host est positionné en fixed (hors flux) au top-center.
|
|
||||||
host.style.cssText =
|
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' })
|
const shadow = host.attachShadow({ mode: 'open' })
|
||||||
shadow.innerHTML = `
|
shadow.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host { all: initial; }
|
.pill {
|
||||||
.wrap {
|
display: flex; align-items: center; gap: 10px;
|
||||||
display: inline-flex; align-items: center; gap: 10px;
|
padding: 10px 18px; border-radius: 999px;
|
||||||
padding: 10px 10px 10px 18px; border-radius: 999px;
|
|
||||||
background: #1c1c1c; color: #faf9f5;
|
background: #1c1c1c; color: #faf9f5;
|
||||||
box-shadow: 0 12px 32px rgba(0,0,0,0.32);
|
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
|
||||||
font-size: 12px; font-weight: 600;
|
font-size: 12px; font-weight: 600;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.02em;
|
||||||
max-width: min(640px, calc(100vw - 24px));
|
|
||||||
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
width: 22px; height: 22px; border-radius: 7px;
|
width: 22px; height: 22px; border-radius: 7px;
|
||||||
background: #faf9f5; color: #1c1c1c;
|
background: #faf9f5; color: #1c1c1c;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
font-family: Georgia, serif; font-weight: 900; font-size: 13px;
|
font-family: Georgia, serif; font-weight: 900; font-size: 12px;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
width: 8px; height: 8px; border-radius: 50%;
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
background: #a47148; animation: pulse 1.2s ease infinite;
|
background: #a47148; animation: pulse 1.2s ease infinite;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.close {
|
|
||||||
width: 22px; height: 22px; border-radius: 50%;
|
|
||||||
background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7);
|
|
||||||
border: none; cursor: pointer; display: flex;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
font-size: 14px; line-height: 1; font-weight: 700;
|
|
||||||
flex-shrink: 0; transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.close:hover {
|
|
||||||
background: rgba(255,255,255,0.18); color: #fff;
|
|
||||||
}
|
}
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -190,26 +154,13 @@
|
|||||||
50% { opacity: 0.5; transform: scale(0.85); }
|
50% { opacity: 0.5; transform: scale(0.85); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="wrap" role="status" aria-live="polite">
|
<div class="pill">
|
||||||
<span class="logo">M</span>
|
<span class="logo">M</span>
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="text">${bannerText.replace(/</g, '<')}</span>
|
<span>${bannerText.replace(/</g, '<')}</span>
|
||||||
<button type="button" class="close" aria-label="${dismissLabel}">×</button>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
document.documentElement.appendChild(host)
|
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) {
|
function setPickMode(enabled) {
|
||||||
@@ -230,21 +181,15 @@
|
|||||||
if (pickMode) paintHighlight()
|
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('selectionchange', broadcastSelection)
|
||||||
document.addEventListener('mouseup', broadcastSelection)
|
document.addEventListener('mouseup', broadcastSelection)
|
||||||
document.addEventListener('keyup', broadcastSelection)
|
document.addEventListener('keyup', broadcastSelection)
|
||||||
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
||||||
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
if (message?.type === 'PING') {
|
if (message?.type === 'PING') {
|
||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'GET_CONTEXT') {
|
if (message?.type === 'GET_CONTEXT') {
|
||||||
@@ -259,24 +204,8 @@
|
|||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true, pickMode })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'DISMISS_BANNER') {
|
|
||||||
bannerDismissed = true
|
|
||||||
removeBanner()
|
|
||||||
sendResponse({ ok: true })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
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()
|
broadcastSelection()
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "__MSG_extDescription__",
|
"description": "__MSG_extDescription__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = false
|
|||||||
const DEFAULT_BASE = 'https://memento-note.com'
|
const DEFAULT_BASE = 'https://memento-note.com'
|
||||||
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
|
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 state = 'idle'
|
||||||
let notebooks = []
|
let notebooks = []
|
||||||
let selectedNotebookId = ''
|
let selectedNotebookId = ''
|
||||||
@@ -160,32 +154,18 @@ async function getActiveTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureContentScript(tabId) {
|
async function ensureContentScript(tabId) {
|
||||||
// PING initial : si le script est déjà injecté, on retourne immédiatement.
|
|
||||||
try {
|
try {
|
||||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||||
if (resp?.ok) return true
|
if (resp?.ok) return true
|
||||||
} catch {
|
} catch {
|
||||||
/* pas injecté */
|
/* inject */
|
||||||
}
|
}
|
||||||
// Sinon, injection manuelle via chrome.scripting.executeScript.
|
|
||||||
try {
|
try {
|
||||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
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) {
|
async function setPickModeOnTab(enabled) {
|
||||||
@@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) {
|
|||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore — retry au prochain syncPickMode */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +246,6 @@ function bindIdleHandlers() {
|
|||||||
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
||||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
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
|
// Gérer l'erreur de chargement du favicon
|
||||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||||
@@ -307,41 +284,11 @@ function updateSelectionUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySelectionFromMessage(msg) {
|
function applySelectionFromMessage(msg) {
|
||||||
if (!msg) return
|
if (!msg || msg.url !== pageUrl) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectionText = msg.text || ''
|
selectionText = msg.text || ''
|
||||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||||
if (msg.lang) pageLang = msg.lang
|
if (msg.lang) pageLang = msg.lang
|
||||||
if (state === 'idle') {
|
if (state === 'idle') updateSelectionUI()
|
||||||
updateSelectionUI()
|
|
||||||
} else {
|
|
||||||
console.debug('[memento] selection updated while busy, deferring UI')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPageContext() {
|
async function refreshPageContext() {
|
||||||
@@ -410,15 +357,12 @@ async function loadSettings() {
|
|||||||
async function loadNotebooks(preferredId) {
|
async function loadNotebooks(preferredId) {
|
||||||
try {
|
try {
|
||||||
await ensureApiPermission()
|
await ensureApiPermission()
|
||||||
const url = `${apiBase()}/api/clip/notebooks`
|
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||||
const res = await fetch(url, { credentials: 'include' })
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
connected = false
|
connected = false
|
||||||
updateConnBadge()
|
updateConnBadge()
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
// Diagnostic : préciser l'URL configurée pour aider l'utilisateur à
|
throw new Error(t('errLoginRequired'))
|
||||||
// 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('errLoadNotebooks'))
|
throw new Error(t('errLoadNotebooks'))
|
||||||
}
|
}
|
||||||
@@ -459,11 +403,7 @@ function renderIdle() {
|
|||||||
|
|
||||||
const authHint =
|
const authHint =
|
||||||
!connected && errorMessage
|
!connected && errorMessage
|
||||||
? `<div class="auth-hint">${escapeHtml(errorMessage).replace(/\n/g, '<br/>')}
|
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||||
<div style="margin-top:8px">
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="openLoginFromHint">${escapeHtml(t('openMomento'))}</button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
els.screen.innerHTML = `
|
els.screen.innerHTML = `
|
||||||
|
|||||||
@@ -1,46 +1,8 @@
|
|||||||
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
|
/** Service worker — ouvre le panneau latéral au clic sur l’icône. */
|
||||||
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
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(() => {})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// === Forwarding content script → side panel ===
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
// En Chrome MV3, chrome.runtime.sendMessage depuis un content script arrive
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
// 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
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Content script Memento — sélection live, surlignage, communication avec le side panel.
|
* 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.
|
* 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 ✕.
|
|
||||||
*/
|
*/
|
||||||
;(function initMementoClipperContent() {
|
;(function initMementoClipperContent() {
|
||||||
if (globalThis.__mementoClipperContent) return
|
if (globalThis.__mementoClipperContent) return
|
||||||
@@ -18,13 +11,8 @@
|
|||||||
const STYLE_ID = 'memento-clipper-styles'
|
const STYLE_ID = 'memento-clipper-styles'
|
||||||
|
|
||||||
let pickMode = false
|
let pickMode = false
|
||||||
let bannerDismissed = false
|
|
||||||
let debounceTimer = null
|
let debounceTimer = null
|
||||||
|
|
||||||
function isRestrictedUrl(url) {
|
|
||||||
return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectionText() {
|
function getSelectionText() {
|
||||||
return window.getSelection()?.toString().trim() || ''
|
return window.getSelection()?.toString().trim() || ''
|
||||||
}
|
}
|
||||||
@@ -125,64 +113,40 @@
|
|||||||
|
|
||||||
function ensureBanner() {
|
function ensureBanner() {
|
||||||
if (document.getElementById(BANNER_ID)) return
|
if (document.getElementById(BANNER_ID)) return
|
||||||
if (!shouldShowBanner()) return
|
|
||||||
|
|
||||||
const bannerText =
|
const bannerText =
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
||||||
'Highlight the text on the page, or clip the whole page.'
|
'Highlight the text to clip'
|
||||||
const dismissLabel =
|
|
||||||
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) ||
|
|
||||||
'Dismiss'
|
|
||||||
|
|
||||||
const host = document.createElement('div')
|
const host = document.createElement('div')
|
||||||
host.id = BANNER_ID
|
host.id = BANNER_ID
|
||||||
// Le host est positionné en fixed (hors flux) au top-center.
|
|
||||||
host.style.cssText =
|
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' })
|
const shadow = host.attachShadow({ mode: 'open' })
|
||||||
shadow.innerHTML = `
|
shadow.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host { all: initial; }
|
.pill {
|
||||||
.wrap {
|
display: flex; align-items: center; gap: 10px;
|
||||||
display: inline-flex; align-items: center; gap: 10px;
|
padding: 10px 18px; border-radius: 999px;
|
||||||
padding: 10px 10px 10px 18px; border-radius: 999px;
|
|
||||||
background: #1c1c1c; color: #faf9f5;
|
background: #1c1c1c; color: #faf9f5;
|
||||||
box-shadow: 0 12px 32px rgba(0,0,0,0.32);
|
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
|
||||||
font-size: 12px; font-weight: 600;
|
font-size: 12px; font-weight: 600;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.02em;
|
||||||
max-width: min(640px, calc(100vw - 24px));
|
|
||||||
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
width: 22px; height: 22px; border-radius: 7px;
|
width: 22px; height: 22px; border-radius: 7px;
|
||||||
background: #faf9f5; color: #1c1c1c;
|
background: #faf9f5; color: #1c1c1c;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
font-family: Georgia, serif; font-weight: 900; font-size: 13px;
|
font-family: Georgia, serif; font-weight: 900; font-size: 12px;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
width: 8px; height: 8px; border-radius: 50%;
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
background: #a47148; animation: pulse 1.2s ease infinite;
|
background: #a47148; animation: pulse 1.2s ease infinite;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.close {
|
|
||||||
width: 22px; height: 22px; border-radius: 50%;
|
|
||||||
background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7);
|
|
||||||
border: none; cursor: pointer; display: flex;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
font-size: 14px; line-height: 1; font-weight: 700;
|
|
||||||
flex-shrink: 0; transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.close:hover {
|
|
||||||
background: rgba(255,255,255,0.18); color: #fff;
|
|
||||||
}
|
}
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -190,26 +154,13 @@
|
|||||||
50% { opacity: 0.5; transform: scale(0.85); }
|
50% { opacity: 0.5; transform: scale(0.85); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="wrap" role="status" aria-live="polite">
|
<div class="pill">
|
||||||
<span class="logo">M</span>
|
<span class="logo">M</span>
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="text">${bannerText.replace(/</g, '<')}</span>
|
<span>${bannerText.replace(/</g, '<')}</span>
|
||||||
<button type="button" class="close" aria-label="${dismissLabel}">×</button>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
document.documentElement.appendChild(host)
|
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) {
|
function setPickMode(enabled) {
|
||||||
@@ -230,21 +181,15 @@
|
|||||||
if (pickMode) paintHighlight()
|
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('selectionchange', broadcastSelection)
|
||||||
document.addEventListener('mouseup', broadcastSelection)
|
document.addEventListener('mouseup', broadcastSelection)
|
||||||
document.addEventListener('keyup', broadcastSelection)
|
document.addEventListener('keyup', broadcastSelection)
|
||||||
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
|
||||||
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
window.addEventListener('resize', onScrollOrResize, { passive: true })
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
if (message?.type === 'PING') {
|
if (message?.type === 'PING') {
|
||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'GET_CONTEXT') {
|
if (message?.type === 'GET_CONTEXT') {
|
||||||
@@ -259,24 +204,8 @@
|
|||||||
sendResponse({ ok: true, pickMode })
|
sendResponse({ ok: true, pickMode })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (message?.type === 'DISMISS_BANNER') {
|
|
||||||
bannerDismissed = true
|
|
||||||
removeBanner()
|
|
||||||
sendResponse({ ok: true })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
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()
|
broadcastSelection()
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "__MSG_extDescription__",
|
"description": "__MSG_extDescription__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -18,21 +18,6 @@
|
|||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"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": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": [
|
||||||
@@ -54,6 +39,21 @@
|
|||||||
"128": "icon-128.png"
|
"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": {
|
"icons": {
|
||||||
"16": "icon-16.png",
|
"16": "icon-16.png",
|
||||||
"48": "icon-48.png",
|
"48": "icon-48.png",
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = false
|
|||||||
const DEFAULT_BASE = 'https://memento-note.com'
|
const DEFAULT_BASE = 'https://memento-note.com'
|
||||||
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
|
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 state = 'idle'
|
||||||
let notebooks = []
|
let notebooks = []
|
||||||
let selectedNotebookId = ''
|
let selectedNotebookId = ''
|
||||||
@@ -160,32 +154,18 @@ async function getActiveTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureContentScript(tabId) {
|
async function ensureContentScript(tabId) {
|
||||||
// PING initial : si le script est déjà injecté, on retourne immédiatement.
|
|
||||||
try {
|
try {
|
||||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||||
if (resp?.ok) return true
|
if (resp?.ok) return true
|
||||||
} catch {
|
} catch {
|
||||||
/* pas injecté */
|
/* inject */
|
||||||
}
|
}
|
||||||
// Sinon, injection manuelle via chrome.scripting.executeScript.
|
|
||||||
try {
|
try {
|
||||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
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) {
|
async function setPickModeOnTab(enabled) {
|
||||||
@@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) {
|
|||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore — retry au prochain syncPickMode */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +246,6 @@ function bindIdleHandlers() {
|
|||||||
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
||||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
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
|
// Gérer l'erreur de chargement du favicon
|
||||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||||
@@ -307,41 +284,11 @@ function updateSelectionUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySelectionFromMessage(msg) {
|
function applySelectionFromMessage(msg) {
|
||||||
if (!msg) return
|
if (!msg || msg.url !== pageUrl) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectionText = msg.text || ''
|
selectionText = msg.text || ''
|
||||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||||
if (msg.lang) pageLang = msg.lang
|
if (msg.lang) pageLang = msg.lang
|
||||||
if (state === 'idle') {
|
if (state === 'idle') updateSelectionUI()
|
||||||
updateSelectionUI()
|
|
||||||
} else {
|
|
||||||
console.debug('[memento] selection updated while busy, deferring UI')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPageContext() {
|
async function refreshPageContext() {
|
||||||
@@ -410,15 +357,12 @@ async function loadSettings() {
|
|||||||
async function loadNotebooks(preferredId) {
|
async function loadNotebooks(preferredId) {
|
||||||
try {
|
try {
|
||||||
await ensureApiPermission()
|
await ensureApiPermission()
|
||||||
const url = `${apiBase()}/api/clip/notebooks`
|
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||||
const res = await fetch(url, { credentials: 'include' })
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
connected = false
|
connected = false
|
||||||
updateConnBadge()
|
updateConnBadge()
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
// Diagnostic : préciser l'URL configurée pour aider l'utilisateur à
|
throw new Error(t('errLoginRequired'))
|
||||||
// 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('errLoadNotebooks'))
|
throw new Error(t('errLoadNotebooks'))
|
||||||
}
|
}
|
||||||
@@ -459,11 +403,7 @@ function renderIdle() {
|
|||||||
|
|
||||||
const authHint =
|
const authHint =
|
||||||
!connected && errorMessage
|
!connected && errorMessage
|
||||||
? `<div class="auth-hint">${escapeHtml(errorMessage).replace(/\n/g, '<br/>')}
|
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||||
<div style="margin-top:8px">
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="openLoginFromHint">${escapeHtml(t('openMomento'))}</button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
els.screen.innerHTML = `
|
els.screen.innerHTML = `
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "__MSG_extDescription__",
|
"description": "__MSG_extDescription__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"permissions": ["activeTab", "scripting", "storage", "sidePanel", "tabs"],
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"scripting",
|
||||||
|
"storage",
|
||||||
|
"sidePanel",
|
||||||
|
"tabs"
|
||||||
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"http://localhost:3000/*",
|
"http://localhost:3000/*",
|
||||||
"http://127.0.0.1:3000/*",
|
"http://127.0.0.1:3000/*",
|
||||||
@@ -18,6 +24,22 @@
|
|||||||
"side_panel": {
|
"side_panel": {
|
||||||
"default_path": "sidepanel.html"
|
"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": {
|
"sidebar_action": {
|
||||||
"default_title": "__MSG_extActionTitle__",
|
"default_title": "__MSG_extActionTitle__",
|
||||||
"default_panel": "sidepanel.html"
|
"default_panel": "sidepanel.html"
|
||||||
@@ -27,16 +49,5 @@
|
|||||||
"id": "memento-clipper@memento-note.com",
|
"id": "memento-clipper@memento-note.com",
|
||||||
"strict_min_version": "115.0"
|
"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__"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3,12 +3,6 @@ const ALLOW_INSTANCE_CONFIG = true
|
|||||||
const DEFAULT_BASE = 'https://memento-note.com'
|
const DEFAULT_BASE = 'https://memento-note.com'
|
||||||
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
|
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 state = 'idle'
|
||||||
let notebooks = []
|
let notebooks = []
|
||||||
let selectedNotebookId = ''
|
let selectedNotebookId = ''
|
||||||
@@ -160,32 +154,18 @@ async function getActiveTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureContentScript(tabId) {
|
async function ensureContentScript(tabId) {
|
||||||
// PING initial : si le script est déjà injecté, on retourne immédiatement.
|
|
||||||
try {
|
try {
|
||||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||||
if (resp?.ok) return true
|
if (resp?.ok) return true
|
||||||
} catch {
|
} catch {
|
||||||
/* pas injecté */
|
/* inject */
|
||||||
}
|
}
|
||||||
// Sinon, injection manuelle via chrome.scripting.executeScript.
|
|
||||||
try {
|
try {
|
||||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
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) {
|
async function setPickModeOnTab(enabled) {
|
||||||
@@ -195,7 +175,7 @@ async function setPickModeOnTab(enabled) {
|
|||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore — retry au prochain syncPickMode */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +246,6 @@ function bindIdleHandlers() {
|
|||||||
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
|
||||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
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
|
// Gérer l'erreur de chargement du favicon
|
||||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||||
@@ -307,41 +284,11 @@ function updateSelectionUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySelectionFromMessage(msg) {
|
function applySelectionFromMessage(msg) {
|
||||||
if (!msg) return
|
if (!msg || msg.url !== pageUrl) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectionText = msg.text || ''
|
selectionText = msg.text || ''
|
||||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||||
if (msg.lang) pageLang = msg.lang
|
if (msg.lang) pageLang = msg.lang
|
||||||
if (state === 'idle') {
|
if (state === 'idle') updateSelectionUI()
|
||||||
updateSelectionUI()
|
|
||||||
} else {
|
|
||||||
console.debug('[memento] selection updated while busy, deferring UI')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPageContext() {
|
async function refreshPageContext() {
|
||||||
@@ -410,15 +357,12 @@ async function loadSettings() {
|
|||||||
async function loadNotebooks(preferredId) {
|
async function loadNotebooks(preferredId) {
|
||||||
try {
|
try {
|
||||||
await ensureApiPermission()
|
await ensureApiPermission()
|
||||||
const url = `${apiBase()}/api/clip/notebooks`
|
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||||
const res = await fetch(url, { credentials: 'include' })
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
connected = false
|
connected = false
|
||||||
updateConnBadge()
|
updateConnBadge()
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
// Diagnostic : préciser l'URL configurée pour aider l'utilisateur à
|
throw new Error(t('errLoginRequired'))
|
||||||
// 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('errLoadNotebooks'))
|
throw new Error(t('errLoadNotebooks'))
|
||||||
}
|
}
|
||||||
@@ -459,11 +403,7 @@ function renderIdle() {
|
|||||||
|
|
||||||
const authHint =
|
const authHint =
|
||||||
!connected && errorMessage
|
!connected && errorMessage
|
||||||
? `<div class="auth-hint">${escapeHtml(errorMessage).replace(/\n/g, '<br/>')}
|
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||||
<div style="margin-top:8px">
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm" id="openLoginFromHint">${escapeHtml(t('openMomento'))}</button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
els.screen.innerHTML = `
|
els.screen.innerHTML = `
|
||||||
|
|||||||
Reference in New Issue
Block a user