fix(extension): sync robuste content/sidepanel, diagnostic panel, preview propre v0.4.4
- content.js : écrit la sélection dans chrome.storage.session/local en plus du message runtime - sidepanel.js : lit le storage en fallback, loggue chaque étape, affiche un diagnostic panel - sidepanel.js : avertissement explicite si le content script n'est pas détecté - sidepanel.js : sépare le rendu Markdown du résumé et l'affichage texte brut de l'extrait - sidepanel.css : agrandit la zone de prévisualisation - i18n : nouvelles clés contentScriptMissing, diagnosticsTitle, copyDiagnostics - locales : correction FR (Retour, Enregistrement, Note, etc.) + version 0.4.4 partout - extract-article.ts : nettoie textContent des métadonnées (dates, temps de lecture, sources) - analyze/route.ts : excerpt propre via makeExcerpt - builds Chrome Store et Firefox regénérés en 0.4.4
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { extractArticleFromHtml } from '@/lib/clip/extract-article'
|
||||
import { extractArticleFromHtml, makeExcerpt } from '@/lib/clip/extract-article'
|
||||
import { analyzeClipContent } from '@/lib/clip/analyze-clip'
|
||||
import { resolveClipLocale, wrapClipPlainParagraph } from '@/lib/clip/rtl-content'
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function POST(request: NextRequest) {
|
||||
tags: analysis.tags,
|
||||
readingTime: analysis.readingTimeMinutes,
|
||||
content: contentHtml,
|
||||
excerpt: textContent.slice(0, 500),
|
||||
excerpt: makeExcerpt(textContent, 500),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[POST /api/clip/analyze]', error)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "الصق عنوان URL الخاص بـ HTTPS (أو LAN) لخادم Memento الخاص بك. تتعامل ملفات تعريف الارتباط الموجودة في هذا المتصفح مع تسجيل الدخول."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<الإصدار>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "لا يستطيع Memento الوصول إلى علامة التبويب هذه. تحقق من أذونات ملحق لوحة المفاتيح/الموقع — أو افتح اللوحة الجانبية."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "ملخص"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Fügen Sie die HTTPS- (oder LAN-)URL Ihres Memento-Servers ein. Cookies in diesem Browser verarbeiten die Anmeldung."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen – oder öffnen Sie den Seitenbereich."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Zusammenfassung"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Paste the HTTPS (or LAN) URL of your Memento server. Cookies in this browser handle sign-in."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Summary"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Pegue la URL HTTPS (o LAN) de su servidor Memento. Las cookies en este navegador controlan el inicio de sesión."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIÓN>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento no puede acceder a esta pestaña. Verifique los permisos de extensión del sitio/teclado o abra el Panel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumen"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکیهای این مرورگر ورود به سیستم را کنترل میکنند."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "خلاصه"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,13 +36,13 @@
|
||||
"message": "Collez l'URL HTTPS (ou LAN) de votre serveur Memento. Les cookies de ce navigateur gèrent la connexion."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento ne peut pas accéder à cet onglet. Vérifiez les autorisations du clavier/extension de site – ou ouvrez le panneau latéral."
|
||||
},
|
||||
"notebookUnnamed": {
|
||||
"message": "Carnet sans titre"
|
||||
"message": "Carnet sans nom"
|
||||
},
|
||||
"noNotebooks": {
|
||||
"message": "Pas encore de carnets"
|
||||
@@ -63,7 +63,7 @@
|
||||
"message": "Sélection détectée"
|
||||
},
|
||||
"ignore": {
|
||||
"message": "ignorer"
|
||||
"message": "Ignorer"
|
||||
},
|
||||
"selectionHint": {
|
||||
"message": "Astuce : surlignez du texte sur la page pour clipper une sélection précise en note."
|
||||
@@ -123,22 +123,22 @@
|
||||
"message": "Enregistrer dans Memento"
|
||||
},
|
||||
"back": {
|
||||
"message": "Dos"
|
||||
"message": "Retour"
|
||||
},
|
||||
"analyzingSource": {
|
||||
"message": "Analyse de la source"
|
||||
},
|
||||
"statusAnalyzing": {
|
||||
"message": "Analyser…"
|
||||
"message": "Analyse en cours…"
|
||||
},
|
||||
"statusSaving": {
|
||||
"message": "Économie…"
|
||||
"message": "Enregistrement…"
|
||||
},
|
||||
"processingDetail": {
|
||||
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
|
||||
},
|
||||
"noteSaved": {
|
||||
"message": "Remarque enregistrée"
|
||||
"message": "Note enregistrée"
|
||||
},
|
||||
"sentToNotebook": {
|
||||
"message": "Enregistré dans $NOTEBOOK$",
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Résumé"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script non détecté — rechargez la page pour clipper."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "अपने मोमेंटो सर्वर का HTTPS (या LAN) URL चिपकाएँ। इस ब्राउज़र में कुकीज़ साइन-इन को संभालती हैं।"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "मोमेंटो वेब क्लिपर <<<संस्करण>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "मोमेंटो इस टैब तक नहीं पहुंच सकता. कीबोर्ड/साइट एक्सटेंशन अनुमतियां जांचें - या साइड पैनल खोलें।"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "सारांश"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Incolla l'URL HTTPS (o LAN) del tuo server Memento. I cookie in questo browser gestiscono l'accesso."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIONE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento non può accedere a questa scheda. Controlla le autorizzazioni per tastiera/estensione del sito oppure apri il pannello laterale."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Riepilogo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento サーバーの HTTPS (または LAN) URL を貼り付けます。このブラウザの Cookie がサインインを処理します。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web クリッパー <<<バージョン>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento はこのタブにアクセスできません。キーボード/サイト拡張機能の権限を確認するか、サイド パネルを開きます。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "要約"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento 서버의 HTTPS(또는 LAN) URL을 붙여넣습니다. 이 브라우저의 쿠키는 로그인을 처리합니다."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<버전>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento는 이 탭에 접근할 수 없습니다. 키보드/사이트 확장 권한을 확인하거나 측면 패널을 엽니다."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "요약"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Plak de HTTPS (of LAN) URL van uw Memento-server. Cookies in deze browser zorgen voor het inloggen."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento heeft geen toegang tot dit tabblad. Controleer de rechten voor toetsenbord-/site-extensies — of open het zijpaneel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Samenvatting"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Wklej adres URL HTTPS (lub LAN) swojego serwera Memento. Pliki cookie w tej przeglądarce obsługują logowanie."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<WERSJA>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento nie ma dostępu do tej karty. Sprawdź uprawnienia rozszerzenia klawiatury/witryny — lub otwórz Panel boczny."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Streszczenie"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Cole o URL HTTPS (ou LAN) do seu servidor Memento. Os cookies neste navegador controlam o login."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSÃO>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento não pode acessar esta guia. Verifique as permissões de extensão de teclado/site – ou abra o painel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Вставьте URL-адрес HTTPS (или LAN) вашего сервера Memento. Файлы cookie в этом браузере обрабатывают вход в систему."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<ВЕРСИЯ>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento не имеет доступа к этой вкладке. Проверьте разрешения для расширения клавиатуры/сайта или откройте боковую панель."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Сводка"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "粘贴 Memento 服务器的 HTTPS(或 LAN)URL。此浏览器中的 Cookie 处理登录。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<版本>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento 无法访问此选项卡。检查键盘/站点扩展权限 - 或打开侧面板。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "摘要"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Architecture v0.4.4 :
|
||||
* - La sélection courante est écrite dans chrome.storage.session/local comme source de vérité
|
||||
* (filet de sécurité si le runtime.sendMessage est perdu).
|
||||
* - Les messages runtime servent de notification rapide au side panel.
|
||||
* - SET_PICK_MODE répond toujours avec l’état pickMode + la sélection courante.
|
||||
*/
|
||||
;(function initMementoClipperContent() {
|
||||
if (globalThis.__mementoClipperContent) return
|
||||
@@ -9,10 +15,16 @@
|
||||
const HIGHLIGHT_ID = 'memento-clipper-highlight-root'
|
||||
const BANNER_ID = 'memento-clipper-banner-root'
|
||||
const STYLE_ID = 'memento-clipper-styles'
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let pickMode = false
|
||||
let debounceTimer = null
|
||||
|
||||
function storageArea() {
|
||||
// Firefox < 128 n’a pas storage.session en MV3 ; on fallback sur local.
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
function getSelectionText() {
|
||||
return window.getSelection()?.toString().trim() || ''
|
||||
}
|
||||
@@ -36,10 +48,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSession(extra = {}) {
|
||||
try {
|
||||
await storageArea().set({
|
||||
[SESSION_KEY]: {
|
||||
...getPageMeta(),
|
||||
...extra,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.debug('[memento] storage session write failed', err)
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastSelection() {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() }
|
||||
writeSession({ source: 'selection' })
|
||||
try {
|
||||
chrome.runtime.sendMessage(payload).catch(() => {})
|
||||
} catch {
|
||||
@@ -189,7 +216,7 @@
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message?.type === 'PING') {
|
||||
sendResponse({ ok: true })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
if (message?.type === 'GET_CONTEXT') {
|
||||
@@ -201,11 +228,13 @@
|
||||
}
|
||||
if (message?.type === 'SET_PICK_MODE') {
|
||||
setPickMode(!!message.enabled)
|
||||
sendResponse({ ok: true, pickMode })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Amorcer le storage dès l'injection (utile si une sélection existait avant l'ouverture du side panel).
|
||||
writeSession({ source: 'init' })
|
||||
broadcastSelection()
|
||||
})()
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "الصق عنوان URL الخاص بـ HTTPS (أو LAN) لخادم Memento الخاص بك. تتعامل ملفات تعريف الارتباط الموجودة في هذا المتصفح مع تسجيل الدخول."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<الإصدار>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "لا يستطيع Memento الوصول إلى علامة التبويب هذه. تحقق من أذونات ملحق لوحة المفاتيح/الموقع — أو افتح اللوحة الجانبية."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "ملخص"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Fügen Sie die HTTPS- (oder LAN-)URL Ihres Memento-Servers ein. Cookies in diesem Browser verarbeiten die Anmeldung."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen – oder öffnen Sie den Seitenbereich."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Zusammenfassung"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Paste the HTTPS (or LAN) URL of your Memento server. Cookies in this browser handle sign-in."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Summary"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Pegue la URL HTTPS (o LAN) de su servidor Memento. Las cookies en este navegador controlan el inicio de sesión."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIÓN>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento no puede acceder a esta pestaña. Verifique los permisos de extensión del sitio/teclado o abra el Panel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumen"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکیهای این مرورگر ورود به سیستم را کنترل میکنند."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "خلاصه"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,13 +36,13 @@
|
||||
"message": "Collez l'URL HTTPS (ou LAN) de votre serveur Memento. Les cookies de ce navigateur gèrent la connexion."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento ne peut pas accéder à cet onglet. Vérifiez les autorisations du clavier/extension de site – ou ouvrez le panneau latéral."
|
||||
},
|
||||
"notebookUnnamed": {
|
||||
"message": "Carnet sans titre"
|
||||
"message": "Carnet sans nom"
|
||||
},
|
||||
"noNotebooks": {
|
||||
"message": "Pas encore de carnets"
|
||||
@@ -63,7 +63,7 @@
|
||||
"message": "Sélection détectée"
|
||||
},
|
||||
"ignore": {
|
||||
"message": "ignorer"
|
||||
"message": "Ignorer"
|
||||
},
|
||||
"selectionHint": {
|
||||
"message": "Astuce : surlignez du texte sur la page pour clipper une sélection précise en note."
|
||||
@@ -123,22 +123,22 @@
|
||||
"message": "Enregistrer dans Memento"
|
||||
},
|
||||
"back": {
|
||||
"message": "Dos"
|
||||
"message": "Retour"
|
||||
},
|
||||
"analyzingSource": {
|
||||
"message": "Analyse de la source"
|
||||
},
|
||||
"statusAnalyzing": {
|
||||
"message": "Analyser…"
|
||||
"message": "Analyse en cours…"
|
||||
},
|
||||
"statusSaving": {
|
||||
"message": "Économie…"
|
||||
"message": "Enregistrement…"
|
||||
},
|
||||
"processingDetail": {
|
||||
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
|
||||
},
|
||||
"noteSaved": {
|
||||
"message": "Remarque enregistrée"
|
||||
"message": "Note enregistrée"
|
||||
},
|
||||
"sentToNotebook": {
|
||||
"message": "Enregistré dans $NOTEBOOK$",
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Résumé"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script non détecté — rechargez la page pour clipper."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "अपने मोमेंटो सर्वर का HTTPS (या LAN) URL चिपकाएँ। इस ब्राउज़र में कुकीज़ साइन-इन को संभालती हैं।"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "मोमेंटो वेब क्लिपर <<<संस्करण>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "मोमेंटो इस टैब तक नहीं पहुंच सकता. कीबोर्ड/साइट एक्सटेंशन अनुमतियां जांचें - या साइड पैनल खोलें।"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "सारांश"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Incolla l'URL HTTPS (o LAN) del tuo server Memento. I cookie in questo browser gestiscono l'accesso."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIONE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento non può accedere a questa scheda. Controlla le autorizzazioni per tastiera/estensione del sito oppure apri il pannello laterale."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Riepilogo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento サーバーの HTTPS (または LAN) URL を貼り付けます。このブラウザの Cookie がサインインを処理します。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web クリッパー <<<バージョン>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento はこのタブにアクセスできません。キーボード/サイト拡張機能の権限を確認するか、サイド パネルを開きます。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "要約"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento 서버의 HTTPS(또는 LAN) URL을 붙여넣습니다. 이 브라우저의 쿠키는 로그인을 처리합니다."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<버전>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento는 이 탭에 접근할 수 없습니다. 키보드/사이트 확장 권한을 확인하거나 측면 패널을 엽니다."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "요약"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Plak de HTTPS (of LAN) URL van uw Memento-server. Cookies in deze browser zorgen voor het inloggen."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento heeft geen toegang tot dit tabblad. Controleer de rechten voor toetsenbord-/site-extensies — of open het zijpaneel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Samenvatting"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Wklej adres URL HTTPS (lub LAN) swojego serwera Memento. Pliki cookie w tej przeglądarce obsługują logowanie."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<WERSJA>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento nie ma dostępu do tej karty. Sprawdź uprawnienia rozszerzenia klawiatury/witryny — lub otwórz Panel boczny."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Streszczenie"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Cole o URL HTTPS (ou LAN) do seu servidor Memento. Os cookies neste navegador controlam o login."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSÃO>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento não pode acessar esta guia. Verifique as permissões de extensão de teclado/site – ou abra o painel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Вставьте URL-адрес HTTPS (или LAN) вашего сервера Memento. Файлы cookie в этом браузере обрабатывают вход в систему."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<ВЕРСИЯ>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento не имеет доступа к этой вкладке. Проверьте разрешения для расширения клавиатуры/сайта или откройте боковую панель."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Сводка"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "粘贴 Memento 服务器的 HTTPS(或 LAN)URL。此浏览器中的 Cookie 处理登录。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<版本>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento 无法访问此选项卡。检查键盘/站点扩展权限 - 或打开侧面板。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "摘要"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Architecture v0.4.4 :
|
||||
* - La sélection courante est écrite dans chrome.storage.session/local comme source de vérité
|
||||
* (filet de sécurité si le runtime.sendMessage est perdu).
|
||||
* - Les messages runtime servent de notification rapide au side panel.
|
||||
* - SET_PICK_MODE répond toujours avec l’état pickMode + la sélection courante.
|
||||
*/
|
||||
;(function initMementoClipperContent() {
|
||||
if (globalThis.__mementoClipperContent) return
|
||||
@@ -9,10 +15,16 @@
|
||||
const HIGHLIGHT_ID = 'memento-clipper-highlight-root'
|
||||
const BANNER_ID = 'memento-clipper-banner-root'
|
||||
const STYLE_ID = 'memento-clipper-styles'
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let pickMode = false
|
||||
let debounceTimer = null
|
||||
|
||||
function storageArea() {
|
||||
// Firefox < 128 n’a pas storage.session en MV3 ; on fallback sur local.
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
function getSelectionText() {
|
||||
return window.getSelection()?.toString().trim() || ''
|
||||
}
|
||||
@@ -36,10 +48,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSession(extra = {}) {
|
||||
try {
|
||||
await storageArea().set({
|
||||
[SESSION_KEY]: {
|
||||
...getPageMeta(),
|
||||
...extra,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.debug('[memento] storage session write failed', err)
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastSelection() {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() }
|
||||
writeSession({ source: 'selection' })
|
||||
try {
|
||||
chrome.runtime.sendMessage(payload).catch(() => {})
|
||||
} catch {
|
||||
@@ -189,7 +216,7 @@
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message?.type === 'PING') {
|
||||
sendResponse({ ok: true })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
if (message?.type === 'GET_CONTEXT') {
|
||||
@@ -201,11 +228,13 @@
|
||||
}
|
||||
if (message?.type === 'SET_PICK_MODE') {
|
||||
setPickMode(!!message.enabled)
|
||||
sendResponse({ ok: true, pickMode })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Amorcer le storage dès l'injection (utile si une sélection existait avant l'ouverture du side panel).
|
||||
writeSession({ source: 'init' })
|
||||
broadcastSelection()
|
||||
})()
|
||||
|
||||
@@ -44,4 +44,13 @@ function applyShellI18n() {
|
||||
if (hint) hint.textContent = t('settingsHint')
|
||||
const footer = document.querySelector('.footer-meta')
|
||||
if (footer) footer.textContent = t('footerVersion')
|
||||
if (typeof els !== 'undefined' && els.diagToggle) {
|
||||
const diagTitle = t('diagnosticsTitle') || 'Diagnostic'
|
||||
els.diagToggle.title = diagTitle
|
||||
els.diagToggle.setAttribute('aria-label', diagTitle)
|
||||
}
|
||||
const diagTitleEl = document.querySelector('.diag-title')
|
||||
if (diagTitleEl) diagTitleEl.textContent = t('diagnosticsTitle') || 'Diagnostic'
|
||||
const copyDiagBtn = document.getElementById('copyDiagBtn')
|
||||
if (copyDiagBtn) copyDiagBtn.textContent = t('copyDiagnostics') || 'Copier'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "__MSG_extDescription__",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
|
||||
@@ -519,12 +519,23 @@ input[type="text"]:focus,
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.excerpt-block .preview-block-body {
|
||||
min-height: 180px;
|
||||
max-height: 360px;
|
||||
min-height: 220px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
background: #fcfbf9;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.excerpt-plain {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.summary-block .preview-block-body {
|
||||
min-height: 120px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
font-style: normal;
|
||||
color: rgba(28, 28, 28, 0.78);
|
||||
background: rgba(2, 132, 199, 0.04);
|
||||
@@ -639,6 +650,77 @@ html[dir="rtl"] .sub {
|
||||
.page-row .title { font-size: 14px; }
|
||||
}
|
||||
|
||||
/* Avertissement content script */
|
||||
.cs-warning {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
color: #9a3412;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Panneau de diagnostic */
|
||||
.diag-panel {
|
||||
border-top: 1px solid var(--border);
|
||||
background: #1c1c1c;
|
||||
color: #e5e5e5;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.diag-panel[hidden] { display: none !important; }
|
||||
.diag-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.diag-title {
|
||||
font-weight: 700;
|
||||
color: #a3a3a3;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.diag-copy {
|
||||
background: transparent;
|
||||
border: 1px solid #525252;
|
||||
color: #d4d4d4;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.diag-copy:hover { background: #404040; }
|
||||
.diag-body {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.diag-line {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.diag-line:last-child { border-bottom: none; }
|
||||
|
||||
.diag-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.diag-toggle:hover { color: var(--ink); }
|
||||
|
||||
/* Très petits écrans (mobile debug, fenetres étirées) */
|
||||
@media (max-width: 320px) {
|
||||
.brand-text { display: none; }
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<span class="conn-dot"></span>
|
||||
<span id="connLabel"></span>
|
||||
</div>
|
||||
<button type="button" id="diagToggle" class="diag-toggle" title="Diagnostic" aria-label="Diagnostic" aria-expanded="false">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</button>
|
||||
<button type="button" id="settingsBtn" class="icon-btn" title="" aria-label="">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||
</button>
|
||||
@@ -53,6 +56,14 @@
|
||||
<footer class="footer">
|
||||
<span class="footer-meta"></span>
|
||||
</footer>
|
||||
|
||||
<div id="diagPanel" class="diag-panel" hidden>
|
||||
<div class="diag-head">
|
||||
<span class="diag-title">Diagnostic</span>
|
||||
<button type="button" id="copyDiagBtn" class="diag-copy">Copier</button>
|
||||
</div>
|
||||
<div id="diagBody" class="diag-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="sidepanel.js"></script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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' }
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let state = 'idle'
|
||||
let notebooks = []
|
||||
@@ -25,6 +26,9 @@ let pendingClipType = 'page'
|
||||
let analyzeResult = null
|
||||
let editableTitle = ''
|
||||
let connected = false
|
||||
let contentScriptReady = false
|
||||
const diagLogs = []
|
||||
let diagOpen = false
|
||||
|
||||
const els = {
|
||||
screen: document.getElementById('screen'),
|
||||
@@ -36,6 +40,10 @@ const els = {
|
||||
settingsStatus: document.getElementById('settingsStatus'),
|
||||
applyInstanceBtn: document.getElementById('applyInstanceBtn'),
|
||||
openLoginBtn: document.getElementById('openLoginBtn'),
|
||||
diagPanel: document.getElementById('diagPanel'),
|
||||
diagToggle: document.getElementById('diagToggle'),
|
||||
diagBody: document.getElementById('diagBody'),
|
||||
copyDiagBtn: document.getElementById('copyDiagBtn'),
|
||||
}
|
||||
|
||||
const ICON_SELECT =
|
||||
@@ -71,6 +79,10 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function escapePlainText(s) {
|
||||
return escapeHtml(s).replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini-renderer Markdown sûr pour le side panel.
|
||||
* - Échappe d'abord le texte (anti-XSS)
|
||||
@@ -136,6 +148,38 @@ function rtlAttrs(text) {
|
||||
return ` class="text-rtl" dir="rtl"${lang}`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostic logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function logDiag(label, detail = '') {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${label}${detail ? ` — ${detail}` : ''}`
|
||||
diagLogs.push(line)
|
||||
if (diagLogs.length > 80) diagLogs.shift()
|
||||
renderDiag()
|
||||
console.debug('[memento-diag]', label, detail)
|
||||
}
|
||||
|
||||
function renderDiag() {
|
||||
if (!els.diagBody) return
|
||||
els.diagBody.innerHTML = diagLogs.map((l) => `<div class="diag-line">${escapeHtml(l)}</div>`).join('')
|
||||
els.diagBody.scrollTop = els.diagBody.scrollHeight
|
||||
}
|
||||
|
||||
async function copyDiagnostics() {
|
||||
try {
|
||||
const text = diagLogs.join('\n')
|
||||
await navigator.clipboard.writeText(text)
|
||||
logDiag('diagnostics copied')
|
||||
} catch (e) {
|
||||
logDiag('copy failed', String(e))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Notebooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sortNotebooksHierarchy(list) {
|
||||
const byParent = new Map()
|
||||
for (const n of list) {
|
||||
@@ -189,29 +233,60 @@ async function getActiveTab() {
|
||||
return tab
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content script synchronization (v0.4.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function storageArea() {
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
async function ensureContentScript(tabId) {
|
||||
logDiag('ensureContentScript start', `tab=${tabId}`)
|
||||
try {
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
if (resp?.ok) return true
|
||||
} catch {
|
||||
/* inject */
|
||||
if (resp?.ok) {
|
||||
contentScriptReady = true
|
||||
logDiag('PING ok', `pickMode=${resp.pickMode} sel=${resp.selection?.text?.length || 0}`)
|
||||
if (resp.selection) applySelectionFromMessage(resp.selection, { fromPing: true })
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
logDiag('PING failed', String(err?.message || err))
|
||||
}
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||
return true
|
||||
} catch {
|
||||
logDiag('content script injected')
|
||||
// attendre un court instant que le script s'enregistre
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('PING after inject', `ok=${contentScriptReady}`)
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('content script inject failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function setPickModeOnTab(enabled) {
|
||||
if (!activeTabId || pageRestricted) return
|
||||
if (!activeTabId || pageRestricted) {
|
||||
logDiag('setPickMode skipped', `tab=${activeTabId} restricted=${pageRestricted}`)
|
||||
return false
|
||||
}
|
||||
const ok = await ensureContentScript(activeTabId)
|
||||
if (!ok) return
|
||||
if (!ok) return false
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
} catch {
|
||||
/* ignore */
|
||||
const resp = await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('SET_PICK_MODE ok', `enabled=${enabled} pickMode=${resp?.pickMode}`)
|
||||
if (resp?.selection) applySelectionFromMessage(resp.selection, { fromSetPickMode: true })
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('SET_PICK_MODE failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +294,158 @@ async function syncPickMode() {
|
||||
await setPickModeOnTab(state === 'idle' && !pageRestricted)
|
||||
}
|
||||
|
||||
async function readSelectionFromStorage() {
|
||||
try {
|
||||
const data = await storageArea().get(SESSION_KEY)
|
||||
const session = data?.[SESSION_KEY]
|
||||
if (!session) return null
|
||||
logDiag('storage read', `url=${session.url?.slice(0, 60)} sel=${session.text?.length || 0}`)
|
||||
return session
|
||||
} catch (err) {
|
||||
logDiag('storage read failed', String(err?.message || err))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg, { fromStorage = false, fromPing = false, fromSetPickMode = false } = {}) {
|
||||
if (!msg) return
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
logDiag('selection applied', `len=${selectionText.length} source=${fromStorage ? 'storage' : fromPing ? 'ping' : fromSetPickMode ? 'setPickMode' : 'message'}`)
|
||||
if (state === 'idle') {
|
||||
if (!curUrl) render()
|
||||
else updateSelectionUI()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
logDiag('refreshPageContext start')
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
logDiag('active tab', `id=${activeTabId} url=${tab?.url?.slice(0, 80)} restricted=${pageRestricted}`)
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
contentScriptReady = false
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) {
|
||||
// En dernier recours, lire depuis le storage session/local.
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
logDiag('GET_CONTEXT ok', `html=${pageHtml.length} sel=${selectionText.length}`)
|
||||
} catch (err) {
|
||||
logDiag('GET_CONTEXT failed', String(err?.message || err))
|
||||
// Fallback storage
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
logDiag('notebooks loaded', `${notebooks.length}`)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
logDiag('notebooks failed', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnBadge() {
|
||||
if (!els.connBadge) return
|
||||
els.connBadge.hidden = !connected
|
||||
@@ -256,7 +483,10 @@ function selectionBlockHtml() {
|
||||
|
||||
function actionsBlockHtml() {
|
||||
const hasSel = Boolean(selectionText)
|
||||
return `<div class="actions" id="actionsSlot">
|
||||
const contentScriptWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
return `${contentScriptWarning}<div class="actions" id="actionsSlot">
|
||||
${
|
||||
hasSel
|
||||
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
||||
@@ -283,7 +513,6 @@ function bindIdleHandlers() {
|
||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
||||
|
||||
// Gérer l'erreur de chargement du favicon
|
||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||
const fallback = this.getAttribute('data-fallback')
|
||||
if (fallback && this.src !== fallback) {
|
||||
@@ -319,160 +548,6 @@ function updateSelectionUI() {
|
||||
bindIdleHandlers()
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg) {
|
||||
if (!msg) return
|
||||
// Comparaison d'URL tolérante : ignorer trailing slash, hash, query.
|
||||
// Compare origin + pathname, pas l'URL complète.
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
// Si pageUrl n'est pas encore set (race avec refreshPageContext),
|
||||
// on accepte le premier message qui arrive et on amorce les métadonnées
|
||||
// de la page depuis le message. Sans ce fallback, le bouton ne s'affiche
|
||||
// jamais si chrome.tabs.query rate ou revient avant refreshPageContext.
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
// URL vraiment différente (autre onglet) — ignorer.
|
||||
return
|
||||
}
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
if (state === 'idle') {
|
||||
// Si la page card n'existait pas encore (pageUrl vide avant),
|
||||
// il faut un render() complet pour qu'elle apparaisse.
|
||||
if (!curUrl) {
|
||||
render()
|
||||
} else {
|
||||
updateSelectionUI()
|
||||
}
|
||||
} else {
|
||||
console.debug('[memento] selection updated while busy, deferring UI')
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
// Ne marquer restricted QUE si on a vraiment une URL à analyser.
|
||||
// tab?.url undefined doit être traité comme "inconnu", pas restricted.
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
} catch {
|
||||
try {
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => ({
|
||||
html: document.documentElement.outerHTML,
|
||||
text: window.getSelection()?.toString().trim() || '',
|
||||
dir: document.documentElement.getAttribute('dir') || '',
|
||||
lang: (document.documentElement.getAttribute('lang') || '').split('-')[0],
|
||||
}),
|
||||
})
|
||||
pageHtml = result?.html || ''
|
||||
selectionText = result?.text || ''
|
||||
pageDir = result?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = result?.lang || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdle() {
|
||||
const restrictedBlock = pageRestricted
|
||||
? `<div class="restricted-note">${escapeHtml(t('restrictedPage'))}</div>`
|
||||
@@ -483,9 +558,14 @@ function renderIdle() {
|
||||
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||
: ''
|
||||
|
||||
const csWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
${restrictedBlock}
|
||||
${authHint}
|
||||
${csWarning}
|
||||
|
||||
<div>
|
||||
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
|
||||
@@ -532,10 +612,10 @@ function renderConfirm() {
|
||||
const reading = formatReadingTime(analyzeResult?.readingTime)
|
||||
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
|
||||
|
||||
// Le résumé et l'extrait peuvent contenir du Markdown produit par l'IA.
|
||||
// On rend le Markdown en HTML (limité et sûr) au lieu de l'afficher brut.
|
||||
// Le résumé peut contenir du Markdown produit par l'IA.
|
||||
// L'extrait est du texte brut : on l'affiche tel quel, sans interpréter de Markdown.
|
||||
const summaryHtml = summary ? renderMarkdownSafe(summary) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? renderMarkdownSafe(excerpt) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? escapePlainText(excerpt) : ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
<div class="confirm-panel">
|
||||
@@ -552,7 +632,7 @@ function renderConfirm() {
|
||||
</section>` : ''}
|
||||
${excerptHtml ? `<section class="preview-block excerpt-block"${rtlAttrs(excerpt)}>
|
||||
<header class="preview-block-head"><span class="dot dot-excerpt"></span>${escapeHtml(t('excerptLabel'))}</header>
|
||||
<div class="preview-block-body md md-excerpt">${excerptHtml}</div>
|
||||
<div class="preview-block-body md md-excerpt excerpt-plain">${excerptHtml}</div>
|
||||
</section>` : ''}
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -670,6 +750,7 @@ async function runAnalyze(type) {
|
||||
analyzeBody = { url: pageUrl, html: pageHtml, title: pageTitle, mode: 'article' }
|
||||
}
|
||||
|
||||
logDiag('analyze request', `${type} url=${pageUrl?.slice(0, 60)}`)
|
||||
const analyzeRes = await fetch(`${apiBase()}/api/clip/analyze`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -682,10 +763,12 @@ async function runAnalyze(type) {
|
||||
analyzeResult = analysis
|
||||
editableTitle = analysis.title || pageTitle || pageDomain
|
||||
state = 'confirm'
|
||||
logDiag('analyze ok', `title=${analysis.title?.slice(0, 40)}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('analyze error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -717,20 +800,34 @@ async function runSave() {
|
||||
lastNoteId = saved.noteId
|
||||
lastNoteUrl = saved.noteUrl
|
||||
state = 'success'
|
||||
logDiag('save ok', `noteId=${lastNoteId}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('save error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg?.type === 'SELECTION_CHANGED') applySelectionFromMessage(msg)
|
||||
if (msg?.type === 'SELECTION_CHANGED') {
|
||||
logDiag('SELECTION_CHANGED received', `len=${msg.text?.length || 0}`)
|
||||
applySelectionFromMessage(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Surveiller le storage session/local en cas de message perdu.
|
||||
storageArea().onChanged.addListener((changes) => {
|
||||
if (changes[SESSION_KEY]?.newValue) {
|
||||
logDiag('storage changed event')
|
||||
applySelectionFromMessage(changes[SESSION_KEY].newValue, { fromStorage: true })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.tabs.onActivated.addListener(async () => {
|
||||
if (state !== 'idle') return
|
||||
logDiag('tabs.onActivated')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -740,6 +837,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
||||
if (info.status !== 'complete' || state !== 'idle') return
|
||||
const tab = await getActiveTab()
|
||||
if (tab?.id === tabId) {
|
||||
logDiag('tabs.onUpdated complete')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -751,6 +849,14 @@ els.settingsBtn?.addEventListener('click', () => {
|
||||
els.settingsPanel.hidden = !els.settingsPanel.hidden
|
||||
})
|
||||
|
||||
els.diagToggle?.addEventListener('click', () => {
|
||||
diagOpen = !diagOpen
|
||||
if (els.diagPanel) els.diagPanel.hidden = !diagOpen
|
||||
if (els.diagToggle) els.diagToggle.setAttribute('aria-expanded', String(diagOpen))
|
||||
})
|
||||
|
||||
els.copyDiagBtn?.addEventListener('click', () => void copyDiagnostics())
|
||||
|
||||
document.querySelectorAll('.preset-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.getAttribute('data-url')
|
||||
@@ -766,12 +872,12 @@ els.openLoginBtn?.addEventListener('click', () => {
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (state === 'idle') {
|
||||
logDiag('visibility visible')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
}
|
||||
} else if (document.visibilityState === 'hidden') {
|
||||
// Désactiver le pick mode quand le sidepanel est fermé
|
||||
await setPickModeOnTab(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "الصق عنوان URL الخاص بـ HTTPS (أو LAN) لخادم Memento الخاص بك. تتعامل ملفات تعريف الارتباط الموجودة في هذا المتصفح مع تسجيل الدخول."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<الإصدار>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "لا يستطيع Memento الوصول إلى علامة التبويب هذه. تحقق من أذونات ملحق لوحة المفاتيح/الموقع — أو افتح اللوحة الجانبية."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "ملخص"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Fügen Sie die HTTPS- (oder LAN-)URL Ihres Memento-Servers ein. Cookies in diesem Browser verarbeiten die Anmeldung."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen – oder öffnen Sie den Seitenbereich."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Zusammenfassung"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Paste the HTTPS (or LAN) URL of your Memento server. Cookies in this browser handle sign-in."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Summary"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Pegue la URL HTTPS (o LAN) de su servidor Memento. Las cookies en este navegador controlan el inicio de sesión."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIÓN>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento no puede acceder a esta pestaña. Verifique los permisos de extensión del sitio/teclado o abra el Panel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumen"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکیهای این مرورگر ورود به سیستم را کنترل میکنند."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "خلاصه"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,13 +36,13 @@
|
||||
"message": "Collez l'URL HTTPS (ou LAN) de votre serveur Memento. Les cookies de ce navigateur gèrent la connexion."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento ne peut pas accéder à cet onglet. Vérifiez les autorisations du clavier/extension de site – ou ouvrez le panneau latéral."
|
||||
},
|
||||
"notebookUnnamed": {
|
||||
"message": "Carnet sans titre"
|
||||
"message": "Carnet sans nom"
|
||||
},
|
||||
"noNotebooks": {
|
||||
"message": "Pas encore de carnets"
|
||||
@@ -63,7 +63,7 @@
|
||||
"message": "Sélection détectée"
|
||||
},
|
||||
"ignore": {
|
||||
"message": "ignorer"
|
||||
"message": "Ignorer"
|
||||
},
|
||||
"selectionHint": {
|
||||
"message": "Astuce : surlignez du texte sur la page pour clipper une sélection précise en note."
|
||||
@@ -123,22 +123,22 @@
|
||||
"message": "Enregistrer dans Memento"
|
||||
},
|
||||
"back": {
|
||||
"message": "Dos"
|
||||
"message": "Retour"
|
||||
},
|
||||
"analyzingSource": {
|
||||
"message": "Analyse de la source"
|
||||
},
|
||||
"statusAnalyzing": {
|
||||
"message": "Analyser…"
|
||||
"message": "Analyse en cours…"
|
||||
},
|
||||
"statusSaving": {
|
||||
"message": "Économie…"
|
||||
"message": "Enregistrement…"
|
||||
},
|
||||
"processingDetail": {
|
||||
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
|
||||
},
|
||||
"noteSaved": {
|
||||
"message": "Remarque enregistrée"
|
||||
"message": "Note enregistrée"
|
||||
},
|
||||
"sentToNotebook": {
|
||||
"message": "Enregistré dans $NOTEBOOK$",
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Résumé"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script non détecté — rechargez la page pour clipper."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "अपने मोमेंटो सर्वर का HTTPS (या LAN) URL चिपकाएँ। इस ब्राउज़र में कुकीज़ साइन-इन को संभालती हैं।"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "मोमेंटो वेब क्लिपर <<<संस्करण>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "मोमेंटो इस टैब तक नहीं पहुंच सकता. कीबोर्ड/साइट एक्सटेंशन अनुमतियां जांचें - या साइड पैनल खोलें।"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "सारांश"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Incolla l'URL HTTPS (o LAN) del tuo server Memento. I cookie in questo browser gestiscono l'accesso."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIONE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento non può accedere a questa scheda. Controlla le autorizzazioni per tastiera/estensione del sito oppure apri il pannello laterale."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Riepilogo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento サーバーの HTTPS (または LAN) URL を貼り付けます。このブラウザの Cookie がサインインを処理します。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web クリッパー <<<バージョン>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento はこのタブにアクセスできません。キーボード/サイト拡張機能の権限を確認するか、サイド パネルを開きます。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "要約"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Memento 서버의 HTTPS(또는 LAN) URL을 붙여넣습니다. 이 브라우저의 쿠키는 로그인을 처리합니다."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<버전>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento는 이 탭에 접근할 수 없습니다. 키보드/사이트 확장 권한을 확인하거나 측면 패널을 엽니다."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "요약"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Plak de HTTPS (of LAN) URL van uw Memento-server. Cookies in deze browser zorgen voor het inloggen."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSIE>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento heeft geen toegang tot dit tabblad. Controleer de rechten voor toetsenbord-/site-extensies — of open het zijpaneel."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Samenvatting"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Wklej adres URL HTTPS (lub LAN) swojego serwera Memento. Pliki cookie w tej przeglądarce obsługują logowanie."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<WERSJA>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento nie ma dostępu do tej karty. Sprawdź uprawnienia rozszerzenia klawiatury/witryny — lub otwórz Panel boczny."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Streszczenie"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Cole o URL HTTPS (ou LAN) do seu servidor Memento. Os cookies neste navegador controlam o login."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<VERSÃO>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento não pode acessar esta guia. Verifique as permissões de extensão de teclado/site – ou abra o painel lateral."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumo"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Вставьте URL-адрес HTTPS (или LAN) вашего сервера Memento. Файлы cookie в этом браузере обрабатывают вход в систему."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<ВЕРСИЯ>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento не имеет доступа к этой вкладке. Проверьте разрешения для расширения клавиатуры/сайта или откройте боковую панель."
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Сводка"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "粘贴 Memento 服务器的 HTTPS(或 LAN)URL。此浏览器中的 Cookie 处理登录。"
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper <<<版本>>>"
|
||||
"message": "Memento Web Clipper 0.4.4"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento 无法访问此选项卡。检查键盘/站点扩展权限 - 或打开侧面板。"
|
||||
@@ -192,5 +192,14 @@
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "摘要"
|
||||
},
|
||||
"contentScriptMissing": {
|
||||
"message": "Content script not detected — reload the page to clip."
|
||||
},
|
||||
"diagnosticsTitle": {
|
||||
"message": "Diagnostic"
|
||||
},
|
||||
"copyDiagnostics": {
|
||||
"message": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Architecture v0.4.4 :
|
||||
* - La sélection courante est écrite dans chrome.storage.session/local comme source de vérité
|
||||
* (filet de sécurité si le runtime.sendMessage est perdu).
|
||||
* - Les messages runtime servent de notification rapide au side panel.
|
||||
* - SET_PICK_MODE répond toujours avec l’état pickMode + la sélection courante.
|
||||
*/
|
||||
;(function initMementoClipperContent() {
|
||||
if (globalThis.__mementoClipperContent) return
|
||||
@@ -9,10 +15,16 @@
|
||||
const HIGHLIGHT_ID = 'memento-clipper-highlight-root'
|
||||
const BANNER_ID = 'memento-clipper-banner-root'
|
||||
const STYLE_ID = 'memento-clipper-styles'
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let pickMode = false
|
||||
let debounceTimer = null
|
||||
|
||||
function storageArea() {
|
||||
// Firefox < 128 n’a pas storage.session en MV3 ; on fallback sur local.
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
function getSelectionText() {
|
||||
return window.getSelection()?.toString().trim() || ''
|
||||
}
|
||||
@@ -36,10 +48,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSession(extra = {}) {
|
||||
try {
|
||||
await storageArea().set({
|
||||
[SESSION_KEY]: {
|
||||
...getPageMeta(),
|
||||
...extra,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.debug('[memento] storage session write failed', err)
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastSelection() {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() }
|
||||
writeSession({ source: 'selection' })
|
||||
try {
|
||||
chrome.runtime.sendMessage(payload).catch(() => {})
|
||||
} catch {
|
||||
@@ -189,7 +216,7 @@
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message?.type === 'PING') {
|
||||
sendResponse({ ok: true })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
if (message?.type === 'GET_CONTEXT') {
|
||||
@@ -201,11 +228,13 @@
|
||||
}
|
||||
if (message?.type === 'SET_PICK_MODE') {
|
||||
setPickMode(!!message.enabled)
|
||||
sendResponse({ ok: true, pickMode })
|
||||
sendResponse({ ok: true, pickMode, selection: getPageMeta() })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Amorcer le storage dès l'injection (utile si une sélection existait avant l'ouverture du side panel).
|
||||
writeSession({ source: 'init' })
|
||||
broadcastSelection()
|
||||
})()
|
||||
|
||||
@@ -44,4 +44,13 @@ function applyShellI18n() {
|
||||
if (hint) hint.textContent = t('settingsHint')
|
||||
const footer = document.querySelector('.footer-meta')
|
||||
if (footer) footer.textContent = t('footerVersion')
|
||||
if (typeof els !== 'undefined' && els.diagToggle) {
|
||||
const diagTitle = t('diagnosticsTitle') || 'Diagnostic'
|
||||
els.diagToggle.title = diagTitle
|
||||
els.diagToggle.setAttribute('aria-label', diagTitle)
|
||||
}
|
||||
const diagTitleEl = document.querySelector('.diag-title')
|
||||
if (diagTitleEl) diagTitleEl.textContent = t('diagnosticsTitle') || 'Diagnostic'
|
||||
const copyDiagBtn = document.getElementById('copyDiagBtn')
|
||||
if (copyDiagBtn) copyDiagBtn.textContent = t('copyDiagnostics') || 'Copier'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "__MSG_extDescription__",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
|
||||
@@ -519,12 +519,23 @@ input[type="text"]:focus,
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.excerpt-block .preview-block-body {
|
||||
min-height: 180px;
|
||||
max-height: 360px;
|
||||
min-height: 220px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
background: #fcfbf9;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.excerpt-plain {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.summary-block .preview-block-body {
|
||||
min-height: 120px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
font-style: normal;
|
||||
color: rgba(28, 28, 28, 0.78);
|
||||
background: rgba(2, 132, 199, 0.04);
|
||||
@@ -639,6 +650,77 @@ html[dir="rtl"] .sub {
|
||||
.page-row .title { font-size: 14px; }
|
||||
}
|
||||
|
||||
/* Avertissement content script */
|
||||
.cs-warning {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
color: #9a3412;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Panneau de diagnostic */
|
||||
.diag-panel {
|
||||
border-top: 1px solid var(--border);
|
||||
background: #1c1c1c;
|
||||
color: #e5e5e5;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.diag-panel[hidden] { display: none !important; }
|
||||
.diag-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.diag-title {
|
||||
font-weight: 700;
|
||||
color: #a3a3a3;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.diag-copy {
|
||||
background: transparent;
|
||||
border: 1px solid #525252;
|
||||
color: #d4d4d4;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.diag-copy:hover { background: #404040; }
|
||||
.diag-body {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.diag-line {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.diag-line:last-child { border-bottom: none; }
|
||||
|
||||
.diag-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.diag-toggle:hover { color: var(--ink); }
|
||||
|
||||
/* Très petits écrans (mobile debug, fenetres étirées) */
|
||||
@media (max-width: 320px) {
|
||||
.brand-text { display: none; }
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<span class="conn-dot"></span>
|
||||
<span id="connLabel"></span>
|
||||
</div>
|
||||
<button type="button" id="diagToggle" class="diag-toggle" title="Diagnostic" aria-label="Diagnostic" aria-expanded="false">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</button>
|
||||
<button type="button" id="settingsBtn" class="icon-btn" title="" aria-label="">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||
</button>
|
||||
@@ -53,6 +56,14 @@
|
||||
<footer class="footer">
|
||||
<span class="footer-meta"></span>
|
||||
</footer>
|
||||
|
||||
<div id="diagPanel" class="diag-panel" hidden>
|
||||
<div class="diag-head">
|
||||
<span class="diag-title">Diagnostic</span>
|
||||
<button type="button" id="copyDiagBtn" class="diag-copy">Copier</button>
|
||||
</div>
|
||||
<div id="diagBody" class="diag-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="sidepanel.js"></script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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' }
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let state = 'idle'
|
||||
let notebooks = []
|
||||
@@ -25,6 +26,9 @@ let pendingClipType = 'page'
|
||||
let analyzeResult = null
|
||||
let editableTitle = ''
|
||||
let connected = false
|
||||
let contentScriptReady = false
|
||||
const diagLogs = []
|
||||
let diagOpen = false
|
||||
|
||||
const els = {
|
||||
screen: document.getElementById('screen'),
|
||||
@@ -36,6 +40,10 @@ const els = {
|
||||
settingsStatus: document.getElementById('settingsStatus'),
|
||||
applyInstanceBtn: document.getElementById('applyInstanceBtn'),
|
||||
openLoginBtn: document.getElementById('openLoginBtn'),
|
||||
diagPanel: document.getElementById('diagPanel'),
|
||||
diagToggle: document.getElementById('diagToggle'),
|
||||
diagBody: document.getElementById('diagBody'),
|
||||
copyDiagBtn: document.getElementById('copyDiagBtn'),
|
||||
}
|
||||
|
||||
const ICON_SELECT =
|
||||
@@ -71,6 +79,10 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function escapePlainText(s) {
|
||||
return escapeHtml(s).replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini-renderer Markdown sûr pour le side panel.
|
||||
* - Échappe d'abord le texte (anti-XSS)
|
||||
@@ -136,6 +148,38 @@ function rtlAttrs(text) {
|
||||
return ` class="text-rtl" dir="rtl"${lang}`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostic logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function logDiag(label, detail = '') {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${label}${detail ? ` — ${detail}` : ''}`
|
||||
diagLogs.push(line)
|
||||
if (diagLogs.length > 80) diagLogs.shift()
|
||||
renderDiag()
|
||||
console.debug('[memento-diag]', label, detail)
|
||||
}
|
||||
|
||||
function renderDiag() {
|
||||
if (!els.diagBody) return
|
||||
els.diagBody.innerHTML = diagLogs.map((l) => `<div class="diag-line">${escapeHtml(l)}</div>`).join('')
|
||||
els.diagBody.scrollTop = els.diagBody.scrollHeight
|
||||
}
|
||||
|
||||
async function copyDiagnostics() {
|
||||
try {
|
||||
const text = diagLogs.join('\n')
|
||||
await navigator.clipboard.writeText(text)
|
||||
logDiag('diagnostics copied')
|
||||
} catch (e) {
|
||||
logDiag('copy failed', String(e))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Notebooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sortNotebooksHierarchy(list) {
|
||||
const byParent = new Map()
|
||||
for (const n of list) {
|
||||
@@ -189,29 +233,60 @@ async function getActiveTab() {
|
||||
return tab
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content script synchronization (v0.4.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function storageArea() {
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
async function ensureContentScript(tabId) {
|
||||
logDiag('ensureContentScript start', `tab=${tabId}`)
|
||||
try {
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
if (resp?.ok) return true
|
||||
} catch {
|
||||
/* inject */
|
||||
if (resp?.ok) {
|
||||
contentScriptReady = true
|
||||
logDiag('PING ok', `pickMode=${resp.pickMode} sel=${resp.selection?.text?.length || 0}`)
|
||||
if (resp.selection) applySelectionFromMessage(resp.selection, { fromPing: true })
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
logDiag('PING failed', String(err?.message || err))
|
||||
}
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||
return true
|
||||
} catch {
|
||||
logDiag('content script injected')
|
||||
// attendre un court instant que le script s'enregistre
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('PING after inject', `ok=${contentScriptReady}`)
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('content script inject failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function setPickModeOnTab(enabled) {
|
||||
if (!activeTabId || pageRestricted) return
|
||||
if (!activeTabId || pageRestricted) {
|
||||
logDiag('setPickMode skipped', `tab=${activeTabId} restricted=${pageRestricted}`)
|
||||
return false
|
||||
}
|
||||
const ok = await ensureContentScript(activeTabId)
|
||||
if (!ok) return
|
||||
if (!ok) return false
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
} catch {
|
||||
/* ignore */
|
||||
const resp = await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('SET_PICK_MODE ok', `enabled=${enabled} pickMode=${resp?.pickMode}`)
|
||||
if (resp?.selection) applySelectionFromMessage(resp.selection, { fromSetPickMode: true })
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('SET_PICK_MODE failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +294,158 @@ async function syncPickMode() {
|
||||
await setPickModeOnTab(state === 'idle' && !pageRestricted)
|
||||
}
|
||||
|
||||
async function readSelectionFromStorage() {
|
||||
try {
|
||||
const data = await storageArea().get(SESSION_KEY)
|
||||
const session = data?.[SESSION_KEY]
|
||||
if (!session) return null
|
||||
logDiag('storage read', `url=${session.url?.slice(0, 60)} sel=${session.text?.length || 0}`)
|
||||
return session
|
||||
} catch (err) {
|
||||
logDiag('storage read failed', String(err?.message || err))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg, { fromStorage = false, fromPing = false, fromSetPickMode = false } = {}) {
|
||||
if (!msg) return
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
logDiag('selection applied', `len=${selectionText.length} source=${fromStorage ? 'storage' : fromPing ? 'ping' : fromSetPickMode ? 'setPickMode' : 'message'}`)
|
||||
if (state === 'idle') {
|
||||
if (!curUrl) render()
|
||||
else updateSelectionUI()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
logDiag('refreshPageContext start')
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
logDiag('active tab', `id=${activeTabId} url=${tab?.url?.slice(0, 80)} restricted=${pageRestricted}`)
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
contentScriptReady = false
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) {
|
||||
// En dernier recours, lire depuis le storage session/local.
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
logDiag('GET_CONTEXT ok', `html=${pageHtml.length} sel=${selectionText.length}`)
|
||||
} catch (err) {
|
||||
logDiag('GET_CONTEXT failed', String(err?.message || err))
|
||||
// Fallback storage
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
logDiag('notebooks loaded', `${notebooks.length}`)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
logDiag('notebooks failed', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnBadge() {
|
||||
if (!els.connBadge) return
|
||||
els.connBadge.hidden = !connected
|
||||
@@ -256,7 +483,10 @@ function selectionBlockHtml() {
|
||||
|
||||
function actionsBlockHtml() {
|
||||
const hasSel = Boolean(selectionText)
|
||||
return `<div class="actions" id="actionsSlot">
|
||||
const contentScriptWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
return `${contentScriptWarning}<div class="actions" id="actionsSlot">
|
||||
${
|
||||
hasSel
|
||||
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
||||
@@ -283,7 +513,6 @@ function bindIdleHandlers() {
|
||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
||||
|
||||
// Gérer l'erreur de chargement du favicon
|
||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||
const fallback = this.getAttribute('data-fallback')
|
||||
if (fallback && this.src !== fallback) {
|
||||
@@ -319,160 +548,6 @@ function updateSelectionUI() {
|
||||
bindIdleHandlers()
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg) {
|
||||
if (!msg) return
|
||||
// Comparaison d'URL tolérante : ignorer trailing slash, hash, query.
|
||||
// Compare origin + pathname, pas l'URL complète.
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
// Si pageUrl n'est pas encore set (race avec refreshPageContext),
|
||||
// on accepte le premier message qui arrive et on amorce les métadonnées
|
||||
// de la page depuis le message. Sans ce fallback, le bouton ne s'affiche
|
||||
// jamais si chrome.tabs.query rate ou revient avant refreshPageContext.
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
// URL vraiment différente (autre onglet) — ignorer.
|
||||
return
|
||||
}
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
if (state === 'idle') {
|
||||
// Si la page card n'existait pas encore (pageUrl vide avant),
|
||||
// il faut un render() complet pour qu'elle apparaisse.
|
||||
if (!curUrl) {
|
||||
render()
|
||||
} else {
|
||||
updateSelectionUI()
|
||||
}
|
||||
} else {
|
||||
console.debug('[memento] selection updated while busy, deferring UI')
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
// Ne marquer restricted QUE si on a vraiment une URL à analyser.
|
||||
// tab?.url undefined doit être traité comme "inconnu", pas restricted.
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
} catch {
|
||||
try {
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => ({
|
||||
html: document.documentElement.outerHTML,
|
||||
text: window.getSelection()?.toString().trim() || '',
|
||||
dir: document.documentElement.getAttribute('dir') || '',
|
||||
lang: (document.documentElement.getAttribute('lang') || '').split('-')[0],
|
||||
}),
|
||||
})
|
||||
pageHtml = result?.html || ''
|
||||
selectionText = result?.text || ''
|
||||
pageDir = result?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = result?.lang || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdle() {
|
||||
const restrictedBlock = pageRestricted
|
||||
? `<div class="restricted-note">${escapeHtml(t('restrictedPage'))}</div>`
|
||||
@@ -483,9 +558,14 @@ function renderIdle() {
|
||||
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||
: ''
|
||||
|
||||
const csWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
${restrictedBlock}
|
||||
${authHint}
|
||||
${csWarning}
|
||||
|
||||
<div>
|
||||
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
|
||||
@@ -532,10 +612,10 @@ function renderConfirm() {
|
||||
const reading = formatReadingTime(analyzeResult?.readingTime)
|
||||
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
|
||||
|
||||
// Le résumé et l'extrait peuvent contenir du Markdown produit par l'IA.
|
||||
// On rend le Markdown en HTML (limité et sûr) au lieu de l'afficher brut.
|
||||
// Le résumé peut contenir du Markdown produit par l'IA.
|
||||
// L'extrait est du texte brut : on l'affiche tel quel, sans interpréter de Markdown.
|
||||
const summaryHtml = summary ? renderMarkdownSafe(summary) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? renderMarkdownSafe(excerpt) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? escapePlainText(excerpt) : ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
<div class="confirm-panel">
|
||||
@@ -552,7 +632,7 @@ function renderConfirm() {
|
||||
</section>` : ''}
|
||||
${excerptHtml ? `<section class="preview-block excerpt-block"${rtlAttrs(excerpt)}>
|
||||
<header class="preview-block-head"><span class="dot dot-excerpt"></span>${escapeHtml(t('excerptLabel'))}</header>
|
||||
<div class="preview-block-body md md-excerpt">${excerptHtml}</div>
|
||||
<div class="preview-block-body md md-excerpt excerpt-plain">${excerptHtml}</div>
|
||||
</section>` : ''}
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -670,6 +750,7 @@ async function runAnalyze(type) {
|
||||
analyzeBody = { url: pageUrl, html: pageHtml, title: pageTitle, mode: 'article' }
|
||||
}
|
||||
|
||||
logDiag('analyze request', `${type} url=${pageUrl?.slice(0, 60)}`)
|
||||
const analyzeRes = await fetch(`${apiBase()}/api/clip/analyze`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -682,10 +763,12 @@ async function runAnalyze(type) {
|
||||
analyzeResult = analysis
|
||||
editableTitle = analysis.title || pageTitle || pageDomain
|
||||
state = 'confirm'
|
||||
logDiag('analyze ok', `title=${analysis.title?.slice(0, 40)}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('analyze error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -717,20 +800,34 @@ async function runSave() {
|
||||
lastNoteId = saved.noteId
|
||||
lastNoteUrl = saved.noteUrl
|
||||
state = 'success'
|
||||
logDiag('save ok', `noteId=${lastNoteId}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('save error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg?.type === 'SELECTION_CHANGED') applySelectionFromMessage(msg)
|
||||
if (msg?.type === 'SELECTION_CHANGED') {
|
||||
logDiag('SELECTION_CHANGED received', `len=${msg.text?.length || 0}`)
|
||||
applySelectionFromMessage(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Surveiller le storage session/local en cas de message perdu.
|
||||
storageArea().onChanged.addListener((changes) => {
|
||||
if (changes[SESSION_KEY]?.newValue) {
|
||||
logDiag('storage changed event')
|
||||
applySelectionFromMessage(changes[SESSION_KEY].newValue, { fromStorage: true })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.tabs.onActivated.addListener(async () => {
|
||||
if (state !== 'idle') return
|
||||
logDiag('tabs.onActivated')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -740,6 +837,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
||||
if (info.status !== 'complete' || state !== 'idle') return
|
||||
const tab = await getActiveTab()
|
||||
if (tab?.id === tabId) {
|
||||
logDiag('tabs.onUpdated complete')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -751,6 +849,14 @@ els.settingsBtn?.addEventListener('click', () => {
|
||||
els.settingsPanel.hidden = !els.settingsPanel.hidden
|
||||
})
|
||||
|
||||
els.diagToggle?.addEventListener('click', () => {
|
||||
diagOpen = !diagOpen
|
||||
if (els.diagPanel) els.diagPanel.hidden = !diagOpen
|
||||
if (els.diagToggle) els.diagToggle.setAttribute('aria-expanded', String(diagOpen))
|
||||
})
|
||||
|
||||
els.copyDiagBtn?.addEventListener('click', () => void copyDiagnostics())
|
||||
|
||||
document.querySelectorAll('.preset-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.getAttribute('data-url')
|
||||
@@ -766,12 +872,12 @@ els.openLoginBtn?.addEventListener('click', () => {
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (state === 'idle') {
|
||||
logDiag('visibility visible')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
}
|
||||
} else if (document.visibilityState === 'hidden') {
|
||||
// Désactiver le pick mode quand le sidepanel est fermé
|
||||
await setPickModeOnTab(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,4 +44,13 @@ function applyShellI18n() {
|
||||
if (hint) hint.textContent = t('settingsHint')
|
||||
const footer = document.querySelector('.footer-meta')
|
||||
if (footer) footer.textContent = t('footerVersion')
|
||||
if (typeof els !== 'undefined' && els.diagToggle) {
|
||||
const diagTitle = t('diagnosticsTitle') || 'Diagnostic'
|
||||
els.diagToggle.title = diagTitle
|
||||
els.diagToggle.setAttribute('aria-label', diagTitle)
|
||||
}
|
||||
const diagTitleEl = document.querySelector('.diag-title')
|
||||
if (diagTitleEl) diagTitleEl.textContent = t('diagnosticsTitle') || 'Diagnostic'
|
||||
const copyDiagBtn = document.getElementById('copyDiagBtn')
|
||||
if (copyDiagBtn) copyDiagBtn.textContent = t('copyDiagnostics') || 'Copier'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "__MSG_extDescription__",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -519,12 +519,23 @@ input[type="text"]:focus,
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.excerpt-block .preview-block-body {
|
||||
min-height: 180px;
|
||||
max-height: 360px;
|
||||
min-height: 220px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
background: #fcfbf9;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.excerpt-plain {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.summary-block .preview-block-body {
|
||||
min-height: 120px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
font-style: normal;
|
||||
color: rgba(28, 28, 28, 0.78);
|
||||
background: rgba(2, 132, 199, 0.04);
|
||||
@@ -639,6 +650,77 @@ html[dir="rtl"] .sub {
|
||||
.page-row .title { font-size: 14px; }
|
||||
}
|
||||
|
||||
/* Avertissement content script */
|
||||
.cs-warning {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
color: #9a3412;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Panneau de diagnostic */
|
||||
.diag-panel {
|
||||
border-top: 1px solid var(--border);
|
||||
background: #1c1c1c;
|
||||
color: #e5e5e5;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.diag-panel[hidden] { display: none !important; }
|
||||
.diag-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.diag-title {
|
||||
font-weight: 700;
|
||||
color: #a3a3a3;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.diag-copy {
|
||||
background: transparent;
|
||||
border: 1px solid #525252;
|
||||
color: #d4d4d4;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.diag-copy:hover { background: #404040; }
|
||||
.diag-body {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.diag-line {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.diag-line:last-child { border-bottom: none; }
|
||||
|
||||
.diag-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.diag-toggle:hover { color: var(--ink); }
|
||||
|
||||
/* Très petits écrans (mobile debug, fenetres étirées) */
|
||||
@media (max-width: 320px) {
|
||||
.brand-text { display: none; }
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<span class="conn-dot"></span>
|
||||
<span id="connLabel"></span>
|
||||
</div>
|
||||
<button type="button" id="diagToggle" class="diag-toggle" title="Diagnostic" aria-label="Diagnostic" aria-expanded="false">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</button>
|
||||
<button type="button" id="settingsBtn" class="icon-btn" title="" aria-label="">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||
</button>
|
||||
@@ -53,6 +56,14 @@
|
||||
<footer class="footer">
|
||||
<span class="footer-meta"></span>
|
||||
</footer>
|
||||
|
||||
<div id="diagPanel" class="diag-panel" hidden>
|
||||
<div class="diag-head">
|
||||
<span class="diag-title">Diagnostic</span>
|
||||
<button type="button" id="copyDiagBtn" class="diag-copy">Copier</button>
|
||||
</div>
|
||||
<div id="diagBody" class="diag-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="sidepanel.js"></script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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' }
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let state = 'idle'
|
||||
let notebooks = []
|
||||
@@ -25,6 +26,9 @@ let pendingClipType = 'page'
|
||||
let analyzeResult = null
|
||||
let editableTitle = ''
|
||||
let connected = false
|
||||
let contentScriptReady = false
|
||||
const diagLogs = []
|
||||
let diagOpen = false
|
||||
|
||||
const els = {
|
||||
screen: document.getElementById('screen'),
|
||||
@@ -36,6 +40,10 @@ const els = {
|
||||
settingsStatus: document.getElementById('settingsStatus'),
|
||||
applyInstanceBtn: document.getElementById('applyInstanceBtn'),
|
||||
openLoginBtn: document.getElementById('openLoginBtn'),
|
||||
diagPanel: document.getElementById('diagPanel'),
|
||||
diagToggle: document.getElementById('diagToggle'),
|
||||
diagBody: document.getElementById('diagBody'),
|
||||
copyDiagBtn: document.getElementById('copyDiagBtn'),
|
||||
}
|
||||
|
||||
const ICON_SELECT =
|
||||
@@ -71,6 +79,10 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function escapePlainText(s) {
|
||||
return escapeHtml(s).replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini-renderer Markdown sûr pour le side panel.
|
||||
* - Échappe d'abord le texte (anti-XSS)
|
||||
@@ -136,6 +148,38 @@ function rtlAttrs(text) {
|
||||
return ` class="text-rtl" dir="rtl"${lang}`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostic logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function logDiag(label, detail = '') {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${label}${detail ? ` — ${detail}` : ''}`
|
||||
diagLogs.push(line)
|
||||
if (diagLogs.length > 80) diagLogs.shift()
|
||||
renderDiag()
|
||||
console.debug('[memento-diag]', label, detail)
|
||||
}
|
||||
|
||||
function renderDiag() {
|
||||
if (!els.diagBody) return
|
||||
els.diagBody.innerHTML = diagLogs.map((l) => `<div class="diag-line">${escapeHtml(l)}</div>`).join('')
|
||||
els.diagBody.scrollTop = els.diagBody.scrollHeight
|
||||
}
|
||||
|
||||
async function copyDiagnostics() {
|
||||
try {
|
||||
const text = diagLogs.join('\n')
|
||||
await navigator.clipboard.writeText(text)
|
||||
logDiag('diagnostics copied')
|
||||
} catch (e) {
|
||||
logDiag('copy failed', String(e))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Notebooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sortNotebooksHierarchy(list) {
|
||||
const byParent = new Map()
|
||||
for (const n of list) {
|
||||
@@ -189,29 +233,60 @@ async function getActiveTab() {
|
||||
return tab
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content script synchronization (v0.4.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function storageArea() {
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
async function ensureContentScript(tabId) {
|
||||
logDiag('ensureContentScript start', `tab=${tabId}`)
|
||||
try {
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
if (resp?.ok) return true
|
||||
} catch {
|
||||
/* inject */
|
||||
if (resp?.ok) {
|
||||
contentScriptReady = true
|
||||
logDiag('PING ok', `pickMode=${resp.pickMode} sel=${resp.selection?.text?.length || 0}`)
|
||||
if (resp.selection) applySelectionFromMessage(resp.selection, { fromPing: true })
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
logDiag('PING failed', String(err?.message || err))
|
||||
}
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||
return true
|
||||
} catch {
|
||||
logDiag('content script injected')
|
||||
// attendre un court instant que le script s'enregistre
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('PING after inject', `ok=${contentScriptReady}`)
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('content script inject failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function setPickModeOnTab(enabled) {
|
||||
if (!activeTabId || pageRestricted) return
|
||||
if (!activeTabId || pageRestricted) {
|
||||
logDiag('setPickMode skipped', `tab=${activeTabId} restricted=${pageRestricted}`)
|
||||
return false
|
||||
}
|
||||
const ok = await ensureContentScript(activeTabId)
|
||||
if (!ok) return
|
||||
if (!ok) return false
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
} catch {
|
||||
/* ignore */
|
||||
const resp = await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('SET_PICK_MODE ok', `enabled=${enabled} pickMode=${resp?.pickMode}`)
|
||||
if (resp?.selection) applySelectionFromMessage(resp.selection, { fromSetPickMode: true })
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('SET_PICK_MODE failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +294,158 @@ async function syncPickMode() {
|
||||
await setPickModeOnTab(state === 'idle' && !pageRestricted)
|
||||
}
|
||||
|
||||
async function readSelectionFromStorage() {
|
||||
try {
|
||||
const data = await storageArea().get(SESSION_KEY)
|
||||
const session = data?.[SESSION_KEY]
|
||||
if (!session) return null
|
||||
logDiag('storage read', `url=${session.url?.slice(0, 60)} sel=${session.text?.length || 0}`)
|
||||
return session
|
||||
} catch (err) {
|
||||
logDiag('storage read failed', String(err?.message || err))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg, { fromStorage = false, fromPing = false, fromSetPickMode = false } = {}) {
|
||||
if (!msg) return
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
logDiag('selection applied', `len=${selectionText.length} source=${fromStorage ? 'storage' : fromPing ? 'ping' : fromSetPickMode ? 'setPickMode' : 'message'}`)
|
||||
if (state === 'idle') {
|
||||
if (!curUrl) render()
|
||||
else updateSelectionUI()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
logDiag('refreshPageContext start')
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
logDiag('active tab', `id=${activeTabId} url=${tab?.url?.slice(0, 80)} restricted=${pageRestricted}`)
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
contentScriptReady = false
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) {
|
||||
// En dernier recours, lire depuis le storage session/local.
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
logDiag('GET_CONTEXT ok', `html=${pageHtml.length} sel=${selectionText.length}`)
|
||||
} catch (err) {
|
||||
logDiag('GET_CONTEXT failed', String(err?.message || err))
|
||||
// Fallback storage
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
logDiag('notebooks loaded', `${notebooks.length}`)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
logDiag('notebooks failed', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnBadge() {
|
||||
if (!els.connBadge) return
|
||||
els.connBadge.hidden = !connected
|
||||
@@ -256,7 +483,10 @@ function selectionBlockHtml() {
|
||||
|
||||
function actionsBlockHtml() {
|
||||
const hasSel = Boolean(selectionText)
|
||||
return `<div class="actions" id="actionsSlot">
|
||||
const contentScriptWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
return `${contentScriptWarning}<div class="actions" id="actionsSlot">
|
||||
${
|
||||
hasSel
|
||||
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
||||
@@ -283,7 +513,6 @@ function bindIdleHandlers() {
|
||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
||||
|
||||
// Gérer l'erreur de chargement du favicon
|
||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||
const fallback = this.getAttribute('data-fallback')
|
||||
if (fallback && this.src !== fallback) {
|
||||
@@ -319,160 +548,6 @@ function updateSelectionUI() {
|
||||
bindIdleHandlers()
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg) {
|
||||
if (!msg) return
|
||||
// Comparaison d'URL tolérante : ignorer trailing slash, hash, query.
|
||||
// Compare origin + pathname, pas l'URL complète.
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
// Si pageUrl n'est pas encore set (race avec refreshPageContext),
|
||||
// on accepte le premier message qui arrive et on amorce les métadonnées
|
||||
// de la page depuis le message. Sans ce fallback, le bouton ne s'affiche
|
||||
// jamais si chrome.tabs.query rate ou revient avant refreshPageContext.
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
// URL vraiment différente (autre onglet) — ignorer.
|
||||
return
|
||||
}
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
if (state === 'idle') {
|
||||
// Si la page card n'existait pas encore (pageUrl vide avant),
|
||||
// il faut un render() complet pour qu'elle apparaisse.
|
||||
if (!curUrl) {
|
||||
render()
|
||||
} else {
|
||||
updateSelectionUI()
|
||||
}
|
||||
} else {
|
||||
console.debug('[memento] selection updated while busy, deferring UI')
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
// Ne marquer restricted QUE si on a vraiment une URL à analyser.
|
||||
// tab?.url undefined doit être traité comme "inconnu", pas restricted.
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
} catch {
|
||||
try {
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => ({
|
||||
html: document.documentElement.outerHTML,
|
||||
text: window.getSelection()?.toString().trim() || '',
|
||||
dir: document.documentElement.getAttribute('dir') || '',
|
||||
lang: (document.documentElement.getAttribute('lang') || '').split('-')[0],
|
||||
}),
|
||||
})
|
||||
pageHtml = result?.html || ''
|
||||
selectionText = result?.text || ''
|
||||
pageDir = result?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = result?.lang || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdle() {
|
||||
const restrictedBlock = pageRestricted
|
||||
? `<div class="restricted-note">${escapeHtml(t('restrictedPage'))}</div>`
|
||||
@@ -483,9 +558,14 @@ function renderIdle() {
|
||||
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||
: ''
|
||||
|
||||
const csWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
${restrictedBlock}
|
||||
${authHint}
|
||||
${csWarning}
|
||||
|
||||
<div>
|
||||
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
|
||||
@@ -532,10 +612,10 @@ function renderConfirm() {
|
||||
const reading = formatReadingTime(analyzeResult?.readingTime)
|
||||
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
|
||||
|
||||
// Le résumé et l'extrait peuvent contenir du Markdown produit par l'IA.
|
||||
// On rend le Markdown en HTML (limité et sûr) au lieu de l'afficher brut.
|
||||
// Le résumé peut contenir du Markdown produit par l'IA.
|
||||
// L'extrait est du texte brut : on l'affiche tel quel, sans interpréter de Markdown.
|
||||
const summaryHtml = summary ? renderMarkdownSafe(summary) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? renderMarkdownSafe(excerpt) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? escapePlainText(excerpt) : ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
<div class="confirm-panel">
|
||||
@@ -552,7 +632,7 @@ function renderConfirm() {
|
||||
</section>` : ''}
|
||||
${excerptHtml ? `<section class="preview-block excerpt-block"${rtlAttrs(excerpt)}>
|
||||
<header class="preview-block-head"><span class="dot dot-excerpt"></span>${escapeHtml(t('excerptLabel'))}</header>
|
||||
<div class="preview-block-body md md-excerpt">${excerptHtml}</div>
|
||||
<div class="preview-block-body md md-excerpt excerpt-plain">${excerptHtml}</div>
|
||||
</section>` : ''}
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -670,6 +750,7 @@ async function runAnalyze(type) {
|
||||
analyzeBody = { url: pageUrl, html: pageHtml, title: pageTitle, mode: 'article' }
|
||||
}
|
||||
|
||||
logDiag('analyze request', `${type} url=${pageUrl?.slice(0, 60)}`)
|
||||
const analyzeRes = await fetch(`${apiBase()}/api/clip/analyze`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -682,10 +763,12 @@ async function runAnalyze(type) {
|
||||
analyzeResult = analysis
|
||||
editableTitle = analysis.title || pageTitle || pageDomain
|
||||
state = 'confirm'
|
||||
logDiag('analyze ok', `title=${analysis.title?.slice(0, 40)}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('analyze error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -717,20 +800,34 @@ async function runSave() {
|
||||
lastNoteId = saved.noteId
|
||||
lastNoteUrl = saved.noteUrl
|
||||
state = 'success'
|
||||
logDiag('save ok', `noteId=${lastNoteId}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('save error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg?.type === 'SELECTION_CHANGED') applySelectionFromMessage(msg)
|
||||
if (msg?.type === 'SELECTION_CHANGED') {
|
||||
logDiag('SELECTION_CHANGED received', `len=${msg.text?.length || 0}`)
|
||||
applySelectionFromMessage(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Surveiller le storage session/local en cas de message perdu.
|
||||
storageArea().onChanged.addListener((changes) => {
|
||||
if (changes[SESSION_KEY]?.newValue) {
|
||||
logDiag('storage changed event')
|
||||
applySelectionFromMessage(changes[SESSION_KEY].newValue, { fromStorage: true })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.tabs.onActivated.addListener(async () => {
|
||||
if (state !== 'idle') return
|
||||
logDiag('tabs.onActivated')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -740,6 +837,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
||||
if (info.status !== 'complete' || state !== 'idle') return
|
||||
const tab = await getActiveTab()
|
||||
if (tab?.id === tabId) {
|
||||
logDiag('tabs.onUpdated complete')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -751,6 +849,14 @@ els.settingsBtn?.addEventListener('click', () => {
|
||||
els.settingsPanel.hidden = !els.settingsPanel.hidden
|
||||
})
|
||||
|
||||
els.diagToggle?.addEventListener('click', () => {
|
||||
diagOpen = !diagOpen
|
||||
if (els.diagPanel) els.diagPanel.hidden = !diagOpen
|
||||
if (els.diagToggle) els.diagToggle.setAttribute('aria-expanded', String(diagOpen))
|
||||
})
|
||||
|
||||
els.copyDiagBtn?.addEventListener('click', () => void copyDiagnostics())
|
||||
|
||||
document.querySelectorAll('.preset-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.getAttribute('data-url')
|
||||
@@ -766,12 +872,12 @@ els.openLoginBtn?.addEventListener('click', () => {
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (state === 'idle') {
|
||||
logDiag('visibility visible')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
}
|
||||
} else if (document.visibilityState === 'hidden') {
|
||||
// Désactiver le pick mode quand le sidepanel est fermé
|
||||
await setPickModeOnTab(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,6 +17,42 @@ export interface ExtractedArticle {
|
||||
locale: ClipLocaleHint
|
||||
}
|
||||
|
||||
const META_PATTERNS = [
|
||||
// Dates/heures avec "mis à jour / updated / actualizado / aktualisiert"
|
||||
/\b(mis à jour|updated at|actualizado|aktualisiert am|aggiornato alle)\b[\s:|-]*.*/gi,
|
||||
// Temps de lecture
|
||||
/\b(temps de lecture|reading time|lesedauer|tiempo de lectura|tempo di lettura|czas czytania|время чтения|阅读时间|読了時間)\b[\s:|-]*.*/gi,
|
||||
// Lignes de type "Source - Aujourd'hui à 09:15 | mis à jour à 11:07"
|
||||
/^(.*?\s[-–|]\s)(aujourd['’]?hui|today|hoy|heute|oggi|vandaag|dzisiaj|сегодня|今日)\b.*$/gim,
|
||||
// Heures seules type "09:15" ou "09:15 | mis à jour..."
|
||||
/^\s*\d{1,2}:\d{2}(\s*[-|]\s*.*)?$/gm,
|
||||
]
|
||||
|
||||
export function cleanClipText(text: string): string {
|
||||
let cleaned = text.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n')
|
||||
for (const pattern of META_PATTERNS) {
|
||||
cleaned = cleaned.replace(pattern, '')
|
||||
}
|
||||
return cleaned
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function makeExcerpt(text: string, maxLen = 500): string {
|
||||
if (text.length <= maxLen) return text
|
||||
// Préférer couper à la fin d'une phrase.
|
||||
const sentenceEnd = text.lastIndexOf('.', maxLen)
|
||||
if (sentenceEnd > maxLen * 0.6) return text.slice(0, sentenceEnd + 1).trim()
|
||||
// Sinon couper au dernier espace.
|
||||
const space = text.lastIndexOf(' ', maxLen)
|
||||
if (space > maxLen * 0.6) return text.slice(0, space).trim() + '…'
|
||||
return text.slice(0, maxLen).trim() + '…'
|
||||
}
|
||||
|
||||
export function extractArticleFromHtml(html: string, pageUrl: string): ExtractedArticle | null {
|
||||
const dom = new JSDOM(html, { url: pageUrl })
|
||||
const reader = new Readability(dom.window.document)
|
||||
@@ -48,11 +84,13 @@ export function extractArticleFromHtml(html: string, pageUrl: string): Extracted
|
||||
const rtlBlocks = applyRtlToHtmlBlocks(sanitized, mergedLocale)
|
||||
const content = wrapClipArticleHtml(rtlBlocks, mergedLocale)
|
||||
|
||||
const cleanText = cleanClipText(article.textContent || '')
|
||||
|
||||
return {
|
||||
title: (article.title || '').trim(),
|
||||
content,
|
||||
textContent: (article.textContent || '').trim(),
|
||||
excerpt: (article.excerpt || '').trim(),
|
||||
textContent: cleanText,
|
||||
excerpt: makeExcerpt(cleanText, 500),
|
||||
locale: mergedLocale,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user