feat(privacy+extension): publier la page /privacy et préparer les builds Chrome/Firefox Store
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m49s
CI / Deploy production (on server) (push) Successful in 24s

- Nouvelle page /privacy bilingue (FR/EN) avec sélection par Accept-Language
  ou ?lang=, sections complètes (données collectées, IA/BYOK, RGPD, sécurité,
  contact). Style sombre aligné sur la landing (x.ai premium).
- Liens footer landing.footer.legal.* mis à jour vers /privacy dans les 15 locales.
- Extension Web Clipper rendue cross-browser :
  * background.js détecte chrome.sidePanel vs browser.sidebarAction
  * sidepanel.js ajoute un shim chrome<->browser pour Firefox
  * manifest.json inclut sidebar_action + browser_specific_settings.gecko
    + optional_host_permissions (Chrome les ignore)
- Script scripts/build-firefox.mjs pour produire un .xpi signé pour AMO,
  symétrique de build-chrome-store.mjs. Exclusion mutuelle des artefacts
  Chrome/Firefox pour éviter la cross-contamination.
- Branding corrigé : 176 occurrences de 'Momento' -> 'Memento' dans le
  bundle de traductions embarqué.
- Version bumpée 0.3.1 -> 0.4.0.
This commit is contained in:
Antigravity
2026-07-18 09:00:44 +00:00
parent 88a7d2ad0a
commit a7c15e9fc3
70 changed files with 5550 additions and 3464 deletions

View File

@@ -0,0 +1,398 @@
import { headers } from 'next/headers'
import Link from 'next/link'
import { parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
export const metadata = {
title: 'Privacy Policy — Memento',
description: 'How Memento handles your data, the Chrome and Firefox web clipper extension, and AI features.',
alternates: { canonical: 'https://memento-note.com/privacy' },
robots: { index: true, follow: true },
}
type Locale = 'en' | 'fr'
const SUPPORTED: Locale[] = ['en', 'fr']
const RTL: Locale[] = []
async function pickLocale(searchLang?: string | string[]): Promise<Locale> {
// 1. Explicit ?lang= override
const raw = Array.isArray(searchLang) ? searchLang[0] : searchLang
if (raw && SUPPORTED.includes(raw.toLowerCase() as Locale)) return raw.toLowerCase() as Locale
// 2. Accept-Language header
try {
const h = await headers()
const accept = parseAcceptLanguage(h.get('accept-language'))
const first = accept?.[0]?.toLowerCase()
if (first && SUPPORTED.includes(first as Locale)) return first as Locale
if (first && first.startsWith('fr')) return 'fr'
} catch {
/* fall through */
}
return 'en'
}
type Section = { id: string; title: string; body: string[] }
type Doc = {
title: string
intro: string
lastUpdated: string
sections: Section[]
}
const DOCS: Record<Locale, Doc> = {
fr: {
title: 'Politique de confidentialité',
intro:
'Cette politique décrit comment Memento (Memento Labs) collecte, utilise et protège vos données, y compris via l\'application web, l\'extension navigateur Web Clipper et les fonctionnalités d\'intelligence artificielle. Nous concevons Memento comme un Second Brain personnel : par défaut, vos données vous appartiennent et ne sont pas partagées.',
lastUpdated: 'Dernière mise à jour : juillet 2026',
sections: [
{
id: 'data-collected',
title: '1. Données collectées',
body: [
'Compte et authentification : adresse e-mail, nom facultatif, mot de passe haché (bcrypt), ou identifiant Google via OAuth (Google Auth Platform).',
'Carnets et notes : contenu, titre, étiquettes, images jointes, pièces jointes (PDF, images, documents), métadonnées (dates, ordre, couleur, épinglé, archivé), et historique des versions (snapshots).',
'Rappels et notifications : date/heure, récurrence, lieu facultatif, état (fait/non fait).',
'Données IA : titre et corps des notes envoyés à votre fournisseur d\'IA configuré pour générer résumés, tags, embeddings, Memory Echo, reformulations, traduction, suggestions. Avec BYOK (votre propre clé), l\'envoi va directement vers votre fournisseur ; sinon, il transite par notre routeur IA mutualisé.',
'Extension Web Clipper (Chrome / Firefox) : lorsque vous cliquez explicitement « Clipper », l\'extension lit l\'URL active, le titre de la page, la sélection de texte et (selon votre choix) le HTML complet de la page. Aucune lecture du contenu de page n\'a lieu tant que vous n\'initiez pas l\'action.',
'Cookies techniques et de session : strictement nécessaires à l\'authentification (cookies NextAuth) et aux préférences (langue, thème, panneau de configuration de l\'extension). Aucun cookie publicitaire, aucun tracker tiers par défaut.',
'Facturation et quotas : pour les tiers payants, référence de commande et jetons d\'utilisation IA stockés pour appliquer les quotas (réservation atomique). Aucune donnée bancaire (traitée par Stripe).',
'Métadonnées techniques : adresse IP (journalisée ~30 jours à des fins anti-abus), user-agent, version de l\'extension, codes d\'erreur anonymisés.',
],
},
{
id: 'usage',
title: '2. Usage des données',
body: [
'Fournir le service : synchronisation de vos carnets, recherche, Memory Echo, Memory Echo insights, agents, publication web de notes, clips depuis l\'extension.',
'Amélioration : journaux d\'erreur anonymisés et métriques agrégées (Prometheus + dashboards Grafana internes) pour diagnostiquer les pannes et améliorer les performances.',
'Sécurité : détection d\'abus, protection contre le credential stuffing et les dépassements de quotas.',
'Nous ne vendons jamais vos données. Nous ne les partageons pas avec des annonceurs. Aucun profilage publicitaire.',
],
},
{
id: 'third-parties',
title: '3. Services tiers et sous-traitants',
body: [
'Google OAuth : authentification optionnelle (Branding → Audience « In production »).',
'Google Cloud Storage (GCP) : hébergement des fichiers joints et des pièces jointes, région Europe (eu-west-3 / europe-west9).',
'PostgreSQL (Docker self-hosted) : base principale, sauvegardes chiffrées au repos.',
'Redis (Docker self-hosted) : cache de session, rate limiting, files de tâches.',
'Fournisseurs d\'IA (OpenAI, Anthropic, Google Gemini, Mistral, OpenRouter, ou votre propre clé BYOK) : envoi du titre et du contenu de la note sélectionnée pour les fonctionnalités IA. Aucun entraînement n\'est effectué sur vos données par notre intermédiaire ; voir la politique du fournisseur retenu.',
'Stripe : facturation des abonnements. Les données de paiement sont gérées exclusivement par Stripe (PCI-DSS).',
'Resend : e-mails transactionnels (vérification de compte, réinitialisation de mot de passe, alertes quota).',
'Google Fonts (CDN fonts.googleapis.com) : polices web sur la landing page et le side panel de l\'extension — adresse IP communiquée à Google.',
'Google Favicons (google.com/s2/favicons) : récupération d\'icônes de sites dans l\'extension — l\'URL du domaine est communiquée à Google.',
'Grafana Cloud (monitoring optionnel) : métriques agrégées anonymisées.',
],
},
{
id: 'extension',
title: '4. Permissions de l\'extension navigateur',
body: [
'activeTab : lecture du contenu de l\'onglet actif uniquement lorsque vous cliquez le bouton « Clipper ». Aucune lecture passive.',
'scripting : injection d\'un petit script de capture de sélection après votre clic explicite.',
'storage : mémorisation de l\'URL de votre serveur Memento et du dernier carnet utilisé.',
'sidePanel (Chrome) / sidebar_action (Firefox) : ouverture du panneau latéral pour afficher l\'interface du clipper.',
'tabs : récupération de l\'URL et du titre de l\'onglet actif pour le contexte du clip.',
'host_permissions : limitées à memento-note.com en production. Les hôtes étendus (localhost / LAN) sont disponibles uniquement en build développeur.',
'L\'extension ne lit jamais l\'historique de navigation. Elle ne communique avec aucun serveur autre que l\'instance Memento que vous avez explicitement configurée.',
],
},
{
id: 'ai',
title: '5. Intelligence artificielle et BYOK',
body: [
'Les fonctionnalités IA (résumé, tags, Memory Echo, embeddings sémantiques, agents) consomment un quota mesuré en tokens. Le quota est réservé atomiquement avant chaque appel.',
'Vous pouvez fournir votre propre clé d\'API (BYOK) pour contourner les quotas mutualisés et faire transiter les appels directement par votre fournisseur. La clé est chiffrée au repos (AES-256-GCM) et n\'est jamais affichée en clair après saisie.',
'Aucun entraînement de modèle n\'est effectué sur vos données par notre intermédiaire.',
'Vous pouvez désactiver individuellement chaque fonctionnalité IA dans Réglages → IA.',
],
},
{
id: 'retention',
title: '6. Conservation des données',
body: [
'Carnets et notes : conservés tant que votre compte est actif. Suppression définitive dans les 30 jours suivant la suppression du compte (sauf obligation légale contraire).',
'Corbeille : notes supprimées conservées 30 jours avant purge définitive.',
'Journaux d\'erreur : 90 jours maximum, agrégés ensuite.',
'Sauvegardes base de données : conservées 30 jours en rotation chiffrée.',
'Métadonnées de facturation : conservées 10 ans (obligation comptable).',
],
},
{
id: 'rights',
title: '7. Vos droits (RGPD / CCPA)',
body: [
'Accès : export complet de vos notes, carnets, étiquettes et paramètres via Réglages → Données.',
'Rectification : édition directe dans l\'interface.',
'Effacement : suppression de compte dans Réglages → Données. Effacement définitif sous 30 jours.',
'Portabilité : export JSON des notes, carnets et étiquettes (équivalent de l\'outil `export_notes` du serveur MCP).',
'Opposition au traitement IA : désactivation des fonctionnalités IA dans Réglages → IA.',
'Réclamation : vous pouvez saisir la CNIL (France) ou l\'autorité de contrôle de votre lieu de résidence.',
'Pour exercer vos droits : privacy@memento-note.com.',
],
},
{
id: 'security',
title: '8. Sécurité',
body: [
'Transport : HTTPS obligatoire, HSTS activé, en-têtes de sécurité (CSP, X-Frame-Options, X-Content-Type-Options).',
'Stockage : mots de passe hachés bcrypt, clés API chiffrées AES-256-GCM, secrets en variables d\'environnement chiffrées au repos.',
'Authentification : cookies de session NextAuth, JWT signés, expiration glissante.',
'Audit : journaux d\'audit log pour les actions sensibles (changement de mot de passe, export de données, suppression de compte).',
'Bug bounty : security@memento-note.com pour signaler une vulnérabilité. Programme responsable de divulgation.',
],
},
{
id: 'minors',
title: '9. Mineurs',
body: [
'Memento n\'est pas destiné aux enfants de moins de 16 ans (RGPD) ou 13 ans (COPPA). Nous ne collectons pas sciemment de données auprès de mineurs. Si vous pensez qu\'un mineur a fourni des données, contactez privacy@memento-note.com pour suppression.',
],
},
{
id: 'changes',
title: '10. Modifications de cette politique',
body: [
'Toute modification substantielle est annoncée par e-mail au moins 30 jours à l\'avance et par bannière dans l\'application. La date de « dernière mise à jour » en haut de cette page est mise à jour à chaque révision.',
'Les modifications non substantielles (corrections de formulation, liens mis à jour) sont publiées sans préavis. La version précédente reste accessible sur demande.',
],
},
{
id: 'contact',
title: '11. Contact et responsable de traitement',
body: [
'Memento Labs — Responsable de traitement : privacy@memento-note.com',
'Délégué à la protection des données (DPO) : dpo@memento-note.com',
'Adresse postale : Memento Labs, [adresse], France',
'Hébergeur des données : serveur self-hosted sur infrastructure Memento Labs (PostgreSQL + Redis Docker, région Europe).',
],
},
],
},
en: {
title: 'Privacy Policy',
intro:
'This policy describes how Memento (Memento Labs) collects, uses and protects your data, including through the web app, the Web Clipper browser extension, and AI features. We design Memento as a personal Second Brain: by default your data belongs to you and is not shared.',
lastUpdated: 'Last updated: July 2026',
sections: [
{
id: 'data-collected',
title: '1. Data we collect',
body: [
'Account and authentication: email address, optional name, hashed password (bcrypt), or Google identifier via OAuth (Google Auth Platform).',
'Notebooks and notes: content, title, labels, attached images and files (PDF, images, documents), metadata (dates, order, color, pinned, archived), and version history (snapshots).',
'Reminders and notifications: date/time, recurrence, optional location, status (done/not done).',
'AI data: note title and body sent to your configured AI provider to generate summaries, tags, embeddings, Memory Echo, reformulations, translations, suggestions. With BYOK (your own key), requests go directly to your provider; otherwise they transit through our shared AI router.',
'Web Clipper extension (Chrome / Firefox): when you explicitly click "Clip", the extension reads the active URL, page title, text selection and (depending on your choice) the full page HTML. No page content is read unless you initiate the action.',
'Technical and session cookies: strictly necessary for authentication (NextAuth cookies) and preferences (language, theme, extension settings panel). No advertising cookies, no third-party trackers by default.',
'Billing and quotas: for paid tiers, order reference and AI usage tokens stored to enforce quotas (atomic reservation). No bank card data (handled by Stripe).',
'Technical metadata: IP address (logged ~30 days for anti-abuse), user agent, extension version, anonymized error codes.',
],
},
{
id: 'usage',
title: '2. How we use data',
body: [
'Provide the service: notebook sync, search, Memory Echo, Memory Echo insights, agents, web publishing of notes, clips from the extension.',
'Improvement: anonymized error logs and aggregated metrics (internal Prometheus + Grafana dashboards) to diagnose failures and improve performance.',
'Security: abuse detection, protection against credential stuffing and quota overruns.',
'We never sell your data. We never share it with advertisers. No advertising profiling.',
],
},
{
id: 'third-parties',
title: '3. Third parties and sub-processors',
body: [
'Google OAuth: optional authentication (Branding → Audience "In production").',
'Google Cloud Storage (GCP): hosting of attached files and attachments, Europe region (eu-west-3 / europe-west9).',
'PostgreSQL (Docker self-hosted): main database, encrypted backups at rest.',
'Redis (Docker self-hosted): session cache, rate limiting, job queues.',
'AI providers (OpenAI, Anthropic, Google Gemini, Mistral, OpenRouter, or your own BYOK key): the title and content of the selected note are sent for AI features. No training is performed on your data through us; see the chosen provider\'s policy.',
'Stripe: subscription billing. Payment data is handled exclusively by Stripe (PCI-DSS).',
'Resend: transactional emails (account verification, password reset, quota alerts).',
'Google Fonts (CDN fonts.googleapis.com): web fonts on the landing page and the extension side panel — IP address is shared with Google.',
'Google Favicons (google.com/s2/favicons): site icon retrieval in the extension — the domain URL is shared with Google.',
'Grafana Cloud (optional monitoring): anonymized aggregated metrics.',
],
},
{
id: 'extension',
title: '4. Browser extension permissions',
body: [
'activeTab: read the active tab content only when you click the "Clip" button. No passive reading.',
'scripting: inject a small selection-capture script after your explicit click.',
'storage: remember your Memento server URL and the last-used notebook.',
'sidePanel (Chrome) / sidebar_action (Firefox): open the side panel to display the clipper UI.',
'tabs: retrieve the active tab URL and title for the clip context.',
'host_permissions: limited to memento-note.com in production. Extended hosts (localhost / LAN) are only available in the developer build.',
'The extension never reads your browsing history. It never communicates with any server other than the Memento instance you explicitly configured.',
],
},
{
id: 'ai',
title: '5. Artificial intelligence and BYOK',
body: [
'AI features (summary, tags, Memory Echo, semantic embeddings, agents) consume a token-based quota. The quota is atomically reserved before each call.',
'You can provide your own API key (BYOK) to bypass shared quotas and route calls directly through your provider. The key is encrypted at rest (AES-256-GCM) and is never displayed in clear text after entry.',
'No model training is performed on your data through us.',
'You can disable each AI feature individually in Settings → AI.',
],
},
{
id: 'retention',
title: '6. Data retention',
body: [
'Notebooks and notes: kept as long as your account is active. Permanently deleted within 30 days of account deletion (unless a legal obligation requires otherwise).',
'Trash: deleted notes kept for 30 days before permanent purge.',
'Error logs: maximum 90 days, then aggregated.',
'Database backups: kept 30 days in encrypted rotation.',
'Billing metadata: kept 10 years (accounting obligation).',
],
},
{
id: 'rights',
title: '7. Your rights (GDPR / CCPA)',
body: [
'Access: full export of your notes, notebooks, labels and settings via Settings → Data.',
'Rectification: edit directly in the interface.',
'Erasure: account deletion in Settings → Data. Definitive erasure within 30 days.',
'Portability: JSON export of notes, notebooks and labels (equivalent of the `export_notes` MCP server tool).',
'Opt-out of AI processing: disable AI features in Settings → AI.',
'Complaint: you may contact the CNIL (France) or the supervisory authority of your place of residence.',
'To exercise your rights: privacy@memento-note.com.',
],
},
{
id: 'security',
title: '8. Security',
body: [
'Transport: HTTPS enforced, HSTS enabled, security headers (CSP, X-Frame-Options, X-Content-Type-Options).',
'Storage: bcrypt-hashed passwords, AES-256-GCM-encrypted API keys, secrets in encrypted environment variables at rest.',
'Authentication: NextAuth session cookies, signed JWT, sliding expiration.',
'Audit: audit logs for sensitive actions (password change, data export, account deletion).',
'Bug bounty: security@memento-note.com to report a vulnerability. Responsible disclosure program.',
],
},
{
id: 'minors',
title: '9. Minors',
body: [
'Memento is not intended for children under 16 (GDPR) or 13 (COPPA). We do not knowingly collect data from minors. If you believe a minor has provided data, contact privacy@memento-note.com for deletion.',
],
},
{
id: 'changes',
title: '10. Changes to this policy',
body: [
'Any substantial change is announced by email at least 30 days in advance and via an in-app banner. The "Last updated" date at the top of this page is updated with each revision.',
'Non-substantial changes (wording fixes, updated links) are published without notice. The previous version remains accessible on request.',
],
},
{
id: 'contact',
title: '11. Contact and data controller',
body: [
'Memento Labs — Data controller: privacy@memento-note.com',
'Data Protection Officer (DPO): dpo@memento-note.com',
'Postal address: Memento Labs, [address], France',
'Data hosting: self-hosted on Memento Labs infrastructure (PostgreSQL + Redis Docker, Europe region).',
],
},
],
},
}
export default async function PrivacyPage({
searchParams,
}: {
searchParams?: Promise<{ lang?: string | string[] }>
}) {
const sp = searchParams ? (await searchParams) : undefined
const locale = await pickLocale(sp?.lang)
const doc = DOCS[locale]
const isRtl = RTL.includes(locale)
return (
<main
lang={locale}
dir={isRtl ? 'rtl' : 'ltr'}
className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white"
>
<header className="px-5 sm:px-8 py-6 border-b border-white/[0.06] flex items-center justify-between">
<Link href="/" className="flex items-center gap-2.5">
<div className="w-9 h-9 bg-[#F4F1EA] text-[#0B0A09] flex items-center justify-center rounded-lg">
<span className="font-serif text-xl font-bold leading-none">M</span>
</div>
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
</Link>
<nav className="flex items-center gap-3">
<a
href="?lang=fr"
aria-current={locale === 'fr' ? 'true' : undefined}
className={`text-xs uppercase tracking-widest px-3 py-1 rounded-full border transition-colors ${
locale === 'fr'
? 'bg-[#F4F1EA] text-[#0B0A09] border-[#F4F1EA]'
: 'border-white/15 text-white/55 hover:border-white/40 hover:text-white'
}`}
>
FR
</a>
<a
href="?lang=en"
aria-current={locale === 'en' ? 'true' : undefined}
className={`text-xs uppercase tracking-widest px-3 py-1 rounded-full border transition-colors ${
locale === 'en'
? 'bg-[#F4F1EA] text-[#0B0A09] border-[#F4F1EA]'
: 'border-white/15 text-white/55 hover:border-white/40 hover:text-white'
}`}
>
EN
</a>
</nav>
</header>
<article className="max-w-3xl mx-auto px-5 sm:px-8 py-16 sm:py-24">
<p className="text-[11px] uppercase tracking-[0.3em] text-[#D4A373] mb-4">Privacy</p>
<h1 className="font-serif text-4xl sm:text-5xl tracking-tight mb-4">{doc.title}</h1>
<p className="text-sm text-white/40 mb-3">{doc.lastUpdated}</p>
<p className="text-white/65 leading-relaxed text-lg mb-12">{doc.intro}</p>
<nav aria-label="Sommaire" className="mb-12 p-5 rounded-xl bg-white/[0.03] border border-white/[0.06]">
<p className="text-[11px] uppercase tracking-[0.2em] text-white/40 mb-3">
{locale === 'fr' ? 'Sommaire' : 'Contents'}
</p>
<ul className="space-y-1.5 text-sm">
{doc.sections.map((s) => (
<li key={s.id}>
<a href={`#${s.id}`} className="text-white/65 hover:text-[#D4A373] transition-colors">
{s.title}
</a>
</li>
))}
</ul>
</nav>
<div className="space-y-12">
{doc.sections.map((s) => (
<section key={s.id} id={s.id} className="scroll-mt-24">
<h2 className="font-serif text-2xl tracking-tight mb-4 text-[#F4F1EA]">{s.title}</h2>
<div className="space-y-3 text-white/70 leading-relaxed">
{s.body.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
</section>
))}
</div>
<footer className="mt-20 pt-8 border-t border-white/[0.06] text-xs text-white/35 flex flex-wrap items-center justify-between gap-4">
<span>© 2026 Memento Labs</span>
<Link href="/" className="hover:text-white transition-colors">
{locale === 'fr' ? 'Retour à l\'accueil' : 'Back to home'}
</Link>
</footer>
</article>
</main>
)
}

View File

@@ -568,7 +568,11 @@ export function LandingPage() {
if (!label || label.startsWith('landing.')) return null
return (
<li key={j}>
{href.startsWith('/') ? (
<Link href={href} className="hover:text-white transition-colors">{label}</Link>
) : (
<a href={href} className="hover:text-white transition-colors">{label}</a>
)}
</li>
)
})}

View File

@@ -10,12 +10,22 @@ Fichiers : `extension/_locales/<lang>/messages.json`. Régénération : `node ex
## Installation (dev)
1. Chrome`chrome://extensions`
### Chrome
1. `chrome://extensions`
2. **Mode développeur****Charger lextension non empaquetée** → dossier `memento-note/extension`
3. Épingle licône Memento
> Chrome **114+** requis (Side Panel API).
### Firefox
```bash
# Avec web-ext (https://extensionworkshop.com/)
npx web-ext run --source-dir memento-note/extension
```
Ou via `about:debugging`**Ce Firefox****Charger un module temporaire** → sélectionner `manifest.json`.
> Firefox **115+** requis (MV3 + sidebarAction).
## Instance Memento
- **Dev** : icône ⚙ → URL (`http://localhost:3000` ou IP LAN) → **Appliquer & reconnecter**
@@ -45,6 +55,30 @@ Fichiers : `extension/_locales/<lang>/messages.json`. Régénération : `node ex
Détection automatique `dir` / `lang` (ex. BBC Persian), aperçu RTL avec Vazirmatn.
## Builds production
Deux scripts produisent des paquets prêts à publier :
```bash
# Chrome Web Store (zip)
node extension/scripts/build-chrome-store.mjs
# → memento-note/extension/memento-web-clipper-chrome-store.zip (~44 Ko, 27 fichiers)
# Firefox Add-ons (xpi)
node extension/scripts/build-firefox.mjs
# → memento-note/extension/memento-web-clipper-firefox.xpi (~44 Ko, 27 fichiers)
```
Chaque build :
- Verrouille `ALLOW_INSTANCE_CONFIG = false` (URL prod en dur)
- Restreint les `host_permissions` à `https://memento-note.com/*`
- Génère les PNG d'icônes depuis `public/icons/icon-{192,512}.svg`
- Exclut les fichiers internes (`i18n/`, `diagnose.js`, `test-sidepanel.html`, etc.)
Différences manifest :
- **Chrome** : `side_panel` (Side Panel API Chrome 114+)
- **Firefox** : `sidebar_action` + `browser_specific_settings.gecko` (Firefox 115+)
## APIs
- `GET /api/clip/notebooks`

View File

@@ -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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen oder öffnen Sie den Seitenbereich."

View File

@@ -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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."

View File

@@ -36,7 +36,7 @@
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکی‌های این مرورگر ورود به سیستم را کنترل می‌کنند."
},
"footerVersion": {
"message": "Memento Web Clipper 0.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."

View File

@@ -9,7 +9,7 @@
"message": "Clipper vers Memento"
},
"webClipper": {
"message": "Web Clipper"
"message": "Tondeuse Web"
},
"connected": {
"message": "Connecté"
@@ -36,7 +36,7 @@
"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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"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."
@@ -78,13 +78,13 @@
"message": "Enregistrer le lien uniquement"
},
"pageNotAccessible": {
"message": "Impossible de clipper ici cette page bloque l'accès aux extensions."
"message": "Impossible de découper ici : cette page bloque l'accès aux extensions."
},
"errLoginRequired": {
"message": "Veuillez d'abord vous connecter à Memento dans ce navigateur."
},
"errLoadNotebooks": {
"message": "Impossible de charger les carnets. Essayez de vous reconnecter."
"message": "Impossible de charger les blocs-notes. Essayez de vous reconnecter."
},
"notebooksLoaded": {
"message": "Carnets chargés"
@@ -123,22 +123,22 @@
"message": "Enregistrer dans Memento"
},
"back": {
"message": "Retour"
"message": "Dos"
},
"analyzingSource": {
"message": "Analyse de la source"
},
"statusAnalyzing": {
"message": "Analyse…"
"message": "Analyser…"
},
"statusSaving": {
"message": "Enregistrement…"
"message": "Économie…"
},
"processingDetail": {
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
},
"noteSaved": {
"message": "Note enregistrée"
"message": "Remarque enregistrée"
},
"sentToNotebook": {
"message": "Enregistré dans $NOTEBOOK$",
@@ -165,7 +165,7 @@
"message": "Réessayer"
},
"errNoSelection": {
"message": "Sélectionnez d'abord du texte, ou clippez la page entière."
"message": "Sélectionnez dabord le texte ou coupez la page entière."
},
"errAnalyzeFailed": {
"message": "Impossible d'analyser cette page."
@@ -177,6 +177,6 @@
"message": "Problème de réseau : vérifiez votre connexion et l'URL Memento."
},
"bannerPickText": {
"message": "Surlignez le texte à clipper"
"message": "Mettez en surbrillance le texte sur la page ou coupez la page entière."
}
}

View File

@@ -1,8 +1,23 @@
/** Service worker — ouvre le panneau latéral au clic sur licône. */
chrome.runtime.onInstalled.addListener(() => {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
})
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
chrome.runtime.onStartup.addListener(() => {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
if (extApi.sidePanel?.setPanelBehavior) {
// Chrome MV3 : Side Panel API
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
} else if (extApi.sidebarAction?.open) {
// Firefox MV3 : sidebarAction
extApi.action?.onClicked?.addListener?.(() => {
extApi.sidebarAction.open().catch(() => {})
})
}
extApi.runtime?.onInstalled?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})
extApi.runtime?.onStartup?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})

View File

@@ -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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen oder öffnen Sie den Seitenbereich."

View File

@@ -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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."

View File

@@ -36,7 +36,7 @@
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکی‌های این مرورگر ورود به سیستم را کنترل می‌کنند."
},
"footerVersion": {
"message": "Memento Web Clipper 0.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."

View File

@@ -9,7 +9,7 @@
"message": "Clipper vers Memento"
},
"webClipper": {
"message": "Web Clipper"
"message": "Tondeuse Web"
},
"connected": {
"message": "Connecté"
@@ -36,7 +36,7 @@
"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.3.1"
"message": "Memento Web Clipper 0.4.0"
},
"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."
@@ -78,13 +78,13 @@
"message": "Enregistrer le lien uniquement"
},
"pageNotAccessible": {
"message": "Impossible de clipper ici cette page bloque l'accès aux extensions."
"message": "Impossible de découper ici : cette page bloque l'accès aux extensions."
},
"errLoginRequired": {
"message": "Veuillez d'abord vous connecter à Memento dans ce navigateur."
},
"errLoadNotebooks": {
"message": "Impossible de charger les carnets. Essayez de vous reconnecter."
"message": "Impossible de charger les blocs-notes. Essayez de vous reconnecter."
},
"notebooksLoaded": {
"message": "Carnets chargés"
@@ -123,22 +123,22 @@
"message": "Enregistrer dans Memento"
},
"back": {
"message": "Retour"
"message": "Dos"
},
"analyzingSource": {
"message": "Analyse de la source"
},
"statusAnalyzing": {
"message": "Analyse…"
"message": "Analyser…"
},
"statusSaving": {
"message": "Enregistrement…"
"message": "Économie…"
},
"processingDetail": {
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
},
"noteSaved": {
"message": "Note enregistrée"
"message": "Remarque enregistrée"
},
"sentToNotebook": {
"message": "Enregistré dans $NOTEBOOK$",
@@ -165,7 +165,7 @@
"message": "Réessayer"
},
"errNoSelection": {
"message": "Sélectionnez d'abord du texte, ou clippez la page entière."
"message": "Sélectionnez dabord le texte ou coupez la page entière."
},
"errAnalyzeFailed": {
"message": "Impossible d'analyser cette page."
@@ -177,6 +177,6 @@
"message": "Problème de réseau : vérifiez votre connexion et l'URL Memento."
},
"bannerPickText": {
"message": "Surlignez le texte à clipper"
"message": "Mettez en surbrillance le texte sur la page ou coupez la page entière."
}
}

View File

@@ -1,8 +1,23 @@
/** Service worker — ouvre le panneau latéral au clic sur licône. */
chrome.runtime.onInstalled.addListener(() => {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
})
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
chrome.runtime.onStartup.addListener(() => {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
if (extApi.sidePanel?.setPanelBehavior) {
// Chrome MV3 : Side Panel API
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
} else if (extApi.sidebarAction?.open) {
// Firefox MV3 : sidebarAction
extApi.action?.onClicked?.addListener?.(() => {
extApi.sidebarAction.open().catch(() => {})
})
}
extApi.runtime?.onInstalled?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})
extApi.runtime?.onStartup?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})

View File

@@ -1,160 +0,0 @@
#!/usr/bin/env node
/**
* Script de diagnostic pour l'extension Memento
* Vérifie tous les fichiers et identifie les problèmes potentiels
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const extDir = __dirname
console.log('🔍 Diagnostic Extension Memento\n')
const issues = []
const warnings = []
// Vérifier la syntaxe des fichiers JS
function checkSyntax(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8')
// Pas de vérification syntaxique simple en Node.js sans eval
// On vérifie juste que le fichier est lisible
return true
} catch (error) {
issues.push(`Fichier illisible: ${filePath} - ${error.message}`)
return false
}
}
// Vérifier les event handlers inline dans le HTML
function checkInlineHandlers(htmlPath) {
try {
const content = fs.readFileSync(htmlPath, 'utf8')
const inlineHandlers = []
if (content.match(/onerror=/i)) inlineHandlers.push('onerror')
if (content.match(/onclick=/i)) inlineHandlers.push('onclick')
if (content.match(/onload=/i)) inlineHandlers.push('onload')
if (inlineHandlers.length > 0) {
issues.push(`Event handlers inline trouvés dans ${htmlPath}: ${inlineHandlers.join(', ')}`)
} else {
console.log('✓ Pas d\'event handlers inline dans le HTML')
}
} catch (error) {
issues.push(`Impossible de lire ${htmlPath}: ${error.message}`)
}
}
// Vérifier les fixes CSP dans sidepanel.js
function checkCSPFixes(jsPath) {
try {
const content = fs.readFileSync(jsPath, 'utf8')
// Vérifier l'absence de onerror inline
if (content.match(/onerror=/)) {
issues.push('Event handler onerror trouvé dans sidepanel.js')
} else {
console.log('✓ Pas de onerror inline dans sidepanel.js')
}
// Vérifier la présence du fix avec data-fallback
if (content.includes('data-fallback')) {
console.log('✓ Fix CSP data-favicon présent')
} else {
warnings.push('Fix CSP data-favicon可能缺失')
}
// Vérifier le handler de favicon
if (content.includes("querySelector('.page-favicon')")) {
console.log('✓ Handler error pour favicon présent')
} else {
warnings.push('Handler error pour favicon可能缺失')
}
} catch (error) {
issues.push(`Impossible de lire ${jsPath}: ${error.message}`)
}
}
// Vérifier le fix pick mode
function checkPickModeFix(jsPath) {
try {
const content = fs.readFileSync(jsPath, 'utf8')
// Vérifier le handler visibilitychange
if (content.includes('visibilityState === \'hidden\'')) {
console.log('✓ Handler visibilitychange pour hidden présent')
} else {
issues.push('Handler visibilitychange pour hidden manquant')
}
// Vérifier l'appel à setPickModeOnTab(false)
if (content.match(/visibilityState === 'hidden'.*setPickModeOnTab\(false\)/s)) {
console.log('✓ Appel setPickModeOnTab(false) dans visibilitychange présent')
} else {
issues.push('Appel setPickModeOnTab(false) dans visibilitychange可能缺失')
}
} catch (error) {
issues.push(`Impossible de lire ${jsPath}: ${error.message}`)
}
}
// Vérifier le manifest
function checkManifest(manifestPath) {
try {
const content = fs.readFileSync(manifestPath, 'utf8')
const manifest = JSON.parse(content)
console.log('✓ Manifest.json valide')
console.log(` Version: ${manifest.version}`)
console.log(` Permissions: ${manifest.permissions.join(', ')}`)
console.log(` Host permissions: ${manifest.host_permissions.length}`)
} catch (error) {
issues.push(`Manifest.json invalide: ${error.message}`)
}
}
// Exécuter les tests
console.log('📋 Vérification des fichiers...\n')
checkSyntax(path.join(extDir, 'sidepanel.js'))
checkSyntax(path.join(extDir, 'content.js'))
checkSyntax(path.join(extDir, 'background.js'))
console.log('\n🔒 Vérification CSP...\n')
checkInlineHandlers(path.join(extDir, 'sidepanel.html'))
checkCSPFixes(path.join(extDir, 'sidepanel.js'))
console.log('\n🎯 Vérification fix pick mode...\n')
checkPickModeFix(path.join(extDir, 'sidepanel.js'))
console.log('\n📦 Vérification manifest...\n')
checkManifest(path.join(extDir, 'manifest.json'))
// Résumé
console.log('\n' + '='.repeat(50))
if (issues.length === 0 && warnings.length === 0) {
console.log('✅ Aucun problème détecté !')
} else {
if (issues.length > 0) {
console.log('\n❌ Problèmes détectés:')
issues.forEach(issue => console.log(`${issue}`))
}
if (warnings.length > 0) {
console.log('\n⚠ Warnings:')
warnings.forEach(warning => console.log(`${warning}`))
}
}
console.log('\n📝 Instructions:')
console.log('1. Rechargez l\'extension dans chrome://extensions (bouton 🔄)')
console.log('2. Ouvrez une page web normale')
console.log('3. Cliquez sur l\'icône Memento')
console.log('4. Fermez le sidepanel - la bannière doit disparaître')
console.log('5. Ouvrez la console (F12) - pas d\'erreur CSP')

View File

@@ -1,215 +0,0 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports */
'use strict'
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const VERSION = "0.3.1"
// Embedded Momento clipper/extension string table (gzip+base64).
// Source English + contextual French anchors; other locales are MT batch
// post-edited with placeholder hardening.
const PACK_B64 = `
H4sIAOZLE2oC/+V9W3MUR7buX6lwTIRnIhDEfjgvxI4dgTGzx2c84A3YjuO3UnchCrW6tKu7pZEmfEKtO0gYbCNuFgYZgS4WqCUk1IAQEVv2u3iTPC/E
CF1AIuYvnG+tzKxLX6qyNTY8nJgxSN1ZmSvX5VvrW1lV/O09K/3eQeNv71l/zR41myz+ucnKZMwG+vm9vzhNVjrrGJ9b9cbhlN3cbLnvfbnPoOEfWpmE
azdnbSddetVhszmbcy2jFVc147OMYaaTxmm74XQK/2WtpJHFBIZNM7c5OddQy6SdrFXvOI0Z4x8dl4yEk05biWzGUMOc1rQ3NGO5LZa7X0lzKEGCnLSz
qbI9kNw0hbySr4Bkajslo0t3KoWwkmXTel/QsKSdqTryqJM1EqHRdjqTNdMJ64SVzdrphkw1tX96/OPQ+E/d1MdmvZUqHf9/gkpUg72rm10rY2U/cZ1k
LlHJXP43hhhq/M+C0WTxbHVkkv0Jp4lnMpubU23HLbmZ0nkO0bdsatcbQlc5zVZaKb/kkmP4KmSYjNTIn+AcZXKamaxlZE9bxp9OnvzkhPF7xzU+PnT0
D7RRwzkVdiXpH8ZhuJMND7TTuNLOGPWu04rvjNOQM2UZGbshXWenhR+dcrBZ9zPLzVTQUoVQMP7WIsZ+KdzQdT+x3CY7Qx99aKXtcldQkyTM9PtZw0wk
8JWQK2vWQ9jTVqLRaLTa6h3TTR7I2NgvnNtKZ9g43uQiPrB9Ui1r5ISdtIxPzLSVEltRkfRpOo2wLpPj03SWQiXpRZy86KgKwHIfDgRnmyUM61pmEsY6
aTdZx9Jlcfd//81ostM5bIHGlV0Aqd0KHuQ6f91v/O7wsU+PnvwdXS8uJi9OmQnrtJNKQuV8HY/hn+Bs0BH5y3u/+7f3GBDMpmZGgvf+13tffikcK2Wx
j39oZStG6Qk1wEiqERx6DWnHLdub/DQ0cSWXPWk3H/SBT8CeIyxGwEiwlCB4MinyEnYGDumJYQI2WenCojTOk7EixnmXeuM/EV9XgENyOZJAbMFssT62
043H0qm2MrXgOyOFLyE3vmU8wVdwlEPsvXZ9BcRl7+aNwcgWO6u3olGfchJwIt+vRRjsVzH0sdNgp49b/52z3XIrfZKyzIwIWw5pJwB74QA/ZbuZbGBS
M1nVuQ87uRSHgpHCMN/R9xsn3TYfzOC54eDK0KwV8V4GSkp8H8gjmKNKIsE3/+i4G845Jx0gftXEQ7v/HbDvdxXDg9A/LjhOZ7PNmYMHDpSD/ZciXDNZ
16alKjnSce9bYVdO2WTzFttkD1eWyTpOqt50Ca9KYSqJOey0SU6rtFa6zIf+kDBamdBZi1VJskP8je/fiK0W22r9wDqFoCWHLt8KfW/U8wAD4UBmUpbm
oqJi2hXlhqhAEpbbnK046oj4zou0k06VZMihVlKo1JuJMo18QJ+xBtJmqq0dsp5A5kuUq0F9bWTE9yxB1szmMt5XVa9RvijGnxAqKRc4MBLQzYCQbgDE
mnaZHv7TSluuSX6OZNeQ2Qd0y1hNJjJRwsjkmppMt20fVw9WU72VpDyR8cONdFMx0MhaMsQy0NpJp5of8QwcMkePnTzywbFjf64cN+rb2OA5jsRkpMws
FYocLuRDH1WrdD4jB7PDxQ5FyyHsrkImZJQ2xXe+I5+CWnPl2cjHL8QupMuK0Q2kcDtxxHWdsvlPQAzAJWzRCnGMVtfBj0i1Cf6srVI9uV+m8KzbVh4/
9KGE2qNO1TQlvhBpkPF5H4FCQmQkyziVS6V4sx5sC3e0/oh9V6rA1bZFIFh+kvEmIKvHXU0eJLbsZ1vah5VtddwyP5IfG6jEchL0uGrjCRTKU1aDJwfq
+P0ynPG9+4mdaDwJHZRO/aeqhUJYT63wV0vtk/3C1SFxqOpr43FH0q7VYAPlrXYURRnJ5YjVgSMkcywlAjDnQuj08oSRNFGZtjgZlLdu2gJxUysH2ByG
Ld/CIEyKmsdpJrylUt13OF1ORwU41d+axO6kk05aOdQO0IEWs1ue0GB2aX9fWszuUBJJNWOFZI5jd0RvkDaFzjx2F5whnuEtT+D/DSnK080+3ds7z7P/
Owf1ww286kh6VBTZy7W4tqtN9w47qRQcL/U+7V8xvpxgfJ46Sn3H+BgempCcD6OgqDSyVANgGqMalsddgruUKcz2VyjhbVO/NCxg5bIIpgxVvssTyDwU
EgmLIr4B6L3f+Azmsk/ZtHtqnuSyjmtnuATKUNwlUtiS5R7wS+gklcRZgqNvDejIgab5YqyCbZo5ylOY09Rjh4c5fIGLWA48URKdCHYIZm5Y6QQVT6Rz
Ef0aFPEfZ/sNjyXiSsoOOblcHFekawM8ETNkglO8Dca4POFRxuWJLIOAFUsa3XjWeCiTzcFxDypoJfhVaAtPSf2j49ryRMI100YzJx3pkjm4VsaXqRkR
z5SStGKKGp26Bai4DERvLdSSZscU/uSxDJOugEdnLT2W6Wcal7w2ZUPQXJpAhoJGj3V+1NTsiG9ox7BIwsmRGHbC/p9bBwPSEAMlBaTep/gbpwD7q09H
NYnoZ1bOZnxKvm/WO24SeJTLeG0+jmgV8ZwWQ0CkR0vDG0qcNt0GVk+GKXSGIRsc9QguaOMMLWQIILIWXT0skzUvsDyR0aGrBJw1sVWR9t8hW6XPDB5i
IeIs9ghFVuHa4KcEXu87uayd8gsXYGlVENXgrhJIyR39wTHklQUVX+uRV5UsXMNsodhOvm/50aTHYl0dFpt1TVuDxQZDmT0/js5+6GS02ayHZjUyWpQJ
OoR2+Sz8z2myLW1OuzyRJpcQFT/ydBN+JGyhclmwXKAw2C38H+jJbJe+hlMkzFQil+KB2oT3OPiyy9crJauME819j/jDhU3eEQd27Ao+EcOCVWaDdl0r
lgqHMTNLNVlaRkEUHf4UCwCOqUZEoFLebbHSlCIcNyM9DkDThJSUkLZusRJVKvIongxzMVq7OmTZy7ZcACDpi0STsmQpAHDiFNceSO9Zm0pcPf4c1NX7
pgyTQJ7UINGhKQKwI1Wjw6fBWupTy+NNbDAKFiAtoXOLVwOLubyCnYJHkIKQwqOp9V9oV+1U/8C29S5SN5vM0yU+9bQYqVeB/L/+mekR95QJYpg2TthM
UTMW6nkqgpLUQ29xkPtPO/Vw4bRxUhyeGh+ddv3TszrEvt1ev7yYwPA0E4FM1kqlsgbynWWA2dTb6WQu3WC05/jKJnzRwPOpGU7wkRktp32sauZO/drn
qiQpdm2l49m3nTidpRZAYLzmyWqd7skqacpT0Ec8uL2upoPVRqZtMKBFa8EiZIM9Eu90q0U7Za+AlYlGtgjDyu1HsO/ly6dOpaV/6VLwPy4vNshLkrY8
dq0DCyfKCh5e9weKQlJRpsSJMuR1ocNXXJ+Bz30gT2cwxnTrhY/T1IfSTVaK3PNt0/FGwIaRZk8iZ2YxjeOMY5bbaLrAhvZcA4x3ysKGlofrUSG5y4un
Amo5aXJN4dYdOIH91B1xW2lfLjaTwWgLU9sN+MUSUcnKW37oG4MMeoI1QaMhiRZHF+GeS5w2nNOIb4QonDf2EBfDGxkPgmihd5b7F8HSG5CuMtLb4ij6
YTN0lLvfu/gtkPNDuUyreTplkBHTkkJG0HKUz3JTcee5zQeNv5huI1/A9mM4Zt+BYdmQ+4xck4BduMpoO3FwJY4JcsG6xxWZ9lwmAePZ8ABNIl4n54k/
4RWOzOLQUrEM/CgSIH1nZJrJB4Eueqz7T0Q8OIjIsTgeoXOZelotNyndPigPnwDjuiz7/hcIL/vUKRV+RiB+9Kj4BzZVLIQf0iIZbABRm14eTZzOZCug
D4DHo3hmWo+N+6e6jUBnwi2BGvBpkxYWe91PmTaDsBSi7OMdBZJvK6DIcqFe2J50pCwff6TshataMYanf6YSgx5P99IuIjX7Lon6EewFZdJpRE66Matc
hlwIvu/Fi7G8WE8E009mdSfamuqpZUzILVC2CrJqEPcvbCuVVggbd9rcyKfNvJAmY3dItibjhAo1sRuZV/Qou0T7KMp+iBCmIZ6xfxS4qy4U/JXY+hc5
SKl3/vxfOXJwcTiXCQCsDl/n4Vpn0M1mYKTWGbSA7hYwiZPM0AmpXXUWneHQ/SKXMZugEqrJKWyp4IJj1qNW4kwefzANhIcbS3VmNTj6f/qjsRy1EN8N
SQ94A5i6ddpKx7H0E15QEvhy3hMIXpp/qnD2PzOeqiKsnqM/5TAZEqAaS98/sOwm5A2OcUsQI7e0ajeACySc8UfrdIqaZblTDeDoKB/SEYz9c4HXTkpK
EU3ZP18exdxCD+05SoOMQbJCyAhUyoTURQiGHZtN2YC29Pj7h4GU2hjSoQohlYF1mDwRHeG3obkCTlw6WfUD8naMbGwmYg+UI+gO4htvm5cL5MWcf1pe
p3laHleF1aJt3lEmitUfdtysmXRck8+/gwRK717o5uURlDCmOD5v466DQ/1oM0XTisZExkjkkNndtOPxqYPAJWp9wN9MumEmx4fmtri/1YY8tZ6aowI1
mrQYe2jHsSfmJGDS0TgxV7tJOlqEXRx+h2WOI+0fm4Y8M8/mVHvONun32g7NLfMMVWl0oTg0T9jLD/Z+ap6yE6ZryBsKSQWxJ+YeENdTMGvcIW015LhZ
GTwy907M4TxlXmN8bPrn5XBCi/abBko0kOnJVlnXSZn4IgVN2gl4Hs1kZUgTe+Tq//7v//7ZkeMnPlr+9uh//Md/1Hx+7hjNOYAy3zZKEW6S0KbRTH8u
z5hcgdunuM2OLCLvmna4jSsP+ciI5FMZG3Y/AP6YohAkNcPZ8TnfoCgypu6JuYxbTJk2ssv3srmUE8fGDy0/pjsajdNmmx/3NdxX7fjH3abm3dV8czXU
f/jY0ZOHjkP56nqjafle2m4yjd9XuwX7D2/tHuyEtE+WUcKMY+6mxoE68CljnQGYCsC1yMgCgeHy1OQV4GygljQ5PIF8dOpginuqhUTiDm0+COCb4U0N
zh7aEB84ZiKp+3G1tnBpIVYsd//PnOkm6aI02cfIIGApViw9Bn+UkomMKW/v5n/nlu8dDIkhj805QviWbQQMKc8PKs1buF04qOsIOPGwxPAb0eU4pMvQ
xUaQ5F26Mct0G7ATwgAvvPYbH7Gb0lF5qoXBQ2FxxtI7LfcyNM+fdDTOy2n+dNLRPjAX6RGyvcvzcml0MSrdYCfNg9I9qNeStE3Sojw559wKxHHNJhtL
mZlArgWuWnRXZxmk6pyfK1j1TtCduNNzKTaPMPXv/s6Qz6flPUQNIp50iHgA6mNPzxMqg0eRcT+WQyVPJSp+KOsuj2ifndvt5IPGqZyHDBpEXFykw8MP
nXbAzGq7F9wDRotPxeE3+8jfck1UDVtNyyN0bzjcB1CRcFGJ4QIEh8Zd4aY0ocweMcRbDGXYeUfn4laZvSPotpcjHPhUKEdU4dceLjrq/nAz/kT8UKoB
l8EDlh8YTSY1ro1UyiJANatV1ZE3iLfonXmLfOmkAym6WeYLx4OfQMZWG9JjzAFNmMK7w5lWgycHppAwQerwy4GYg26riTWGtHiQzsBknZrJcRr6K8dD
myrgSzUbTYePl5Y2yZCifO1lnaQZ+EayXzv7WzwHnOXiEiWIaArVG3TenqEU30KcvN02+Y5YGzp08AfKihzKggD5tcW5PzX44YEYJR/srJn2QsWa94mb
DSkSi24Hs41TDt17S/dEqOf9IkgwB0jW0b9tPJP5DUnw+/Kq1Ps2N8BM2k+NBNim+zuyZjtHJC5nGix++xfuHUflBD81oFdpW1BHbSrcrMWEP0onYA6z
5O5xRYUrepLxkSTCdGCEqCQ3VQ8TNlDxQQZ1xF2jZLl/kf0eO3pkT+wXRCS3PKvYL8JCyGpSp8tKmnTozpydNm/Je8fb29mENpFhRBn2gux7gGpsvuXV
UmwYINvcTN0qE6Br2Clx52Mq5ajKzdJiwydFHJOGye2ymDieDh+1qNWuMIAAGnBl1saHbWw5q82HE+Hjab7WeVs0VwSRixTTYmY1SK7Gw8Yncg0NFiVL
hk4PYcmOAnQzuVRKorHJruDaWUY7WFyRXSFXgOtatXBdcTUNi6a5cl3lukIkjWeRUy106yf8EXtKCLwN4JnuHeJQenDroKkHw5LwCXXC9GMdySf1vh8v
ekSXBGHMpMaKGXxQOYwutd0NTsIDQAWI+jlzvwEgb6F1ArCatbU47UmVd8W8WVvnFnAGK6uGm8BZje/4FnCybcpugu2zRGeVG2aJuIbpLD43MBlxkawd
4LLVQVGDz3rACLeQwzli4m8JZ06brYHTYirP9/BDhkLHe5An5mzZ0WG0UI6Z1WG0MmpzsTeCf5RO2iAPjjajzdhc35io0mritExq9+/fH0tqj9uZZhOJ
uUZe65VLcC8wWttqtlMO0Sn5uLOktI7bDGRnB9OhtGzCrKn1qDONZKR5Vw87Z3JC0TU88uwlhVz6fTAZN5QWYm73ZlBU3Db+6ef/gnQJB/lteZye0yVd
AY9d5iIpwzUbGmw65ncZEbI501AVtAbTtZsJhzW5LnmKKaPUS9OOD0yo3elWAKWK2u7rtuXNF2QHTnOBDFfDvd00jYQPUkdtVNc26IT9oDpISpnes5ci
fxhW7fdyH4mpbCL1x1Gc/e3eeeWd8yZNj/AKNk79K24zNQEIcuIhLnEsRc/5m8tjy/NW8JHpb2Vr2qrLYAihBx3+Lj+At+z9+NcSZxx6VFhRFmzl81/x
CHj59p4OgZ2a6C9fQmnH5F7V8g+iWeXUyoCT1ilURjDObcc7Bxa//WvHwNaejoG1uO9hejuAY1R6bFp4Ufkp8DH/EDhd5Qi4CVOmqNz913jvctexvdBe
Unizw6e+9K1s2zXk7NB5r6mOezMcS96JrzSePOk9EHxUms98Mbdp13zmq458EdC6R742ko3YzOnlEZTbez30tWs99A09Ke1kgrO8JcKrQuhXPNb9EJF0
UCArWd+RUJuueqDbJDiuECV0nktf1cBzPTygkZbGka5V24kuV3AuNbsoldDrtzTPckms5QmDMvfyvRYqZgInujY7fYUzXZtCwBRMd/mWHzR6PPePyF2m
wAa6n8F/0UEYSLiPb7uO5mEu7eSUYwe2kqBndOkEAv4bfEcXn+f6aKr50LN/iuta/9+c48Ly5ALqKAD8AzmM7ukJnOOe4nNceYzrBB+FTlbGSa0Hofd6
jlvjMa7ln+KKmvVXPMQ9Sc/y6BJeNxAKVRnvZ05KpX8tvitOcGsiuxndA1yWOjA2nuq6pgeD8lFnOrvlhIhyq5TnivpWm+hq0lwup9/hqW2TNrv1Tm1z
use2FRDQSSdSwGC909skSj2KZYIgh95SRdgJeK9aE0fQWgJZ9umWwK1FGme5zPGc4Emu7TKWKAjyUxExNdvVPcktV44p3L32E93yqQR0sKL2cLIrasvg
6a64y+ivtJBlSDJTA+H9MKLCiVElR1tKk+4mtNnuxxa9OK5esOr3+c7RBno3QaNFt4hDysZM1mgxxcNXuVbv1m7/HbJ1RpNpNnrPr9Jb55qsLI3lx5K9
K2p8z3PaNF39J5ITOg8kHzLpgaQUJNd5JNkSW3J+syeSP/W1KYZmbauu1lc9Wy2O48rnke09P4980rGaxUPr+L/TDAIH82k/kOwVFs30FHr8vc0ps5EC
TFHaU/67n5G3gk5W4dXPSavd8g5z2+lhenoAyKGH3clFUbt6D/Xs+UB3L8e5py3rFD1jQe9lcoDP9G5MJ2sk7Sy9ELoebNU7z7X4gTaDnydW0uOaLNRP
r4WoY1ZbJ2/GtOQbok+JN0TTLtvtM3QTnuYLoo+aZlPKEYW2DX+pd6zGM1b8A8YNYjOhq2rgt7ksaGm75uPFxxCXLRZZOniKS88Yt7+dZ4xl6qNHSuQ7
o4F+EcQ2bTVoP2Z80JBwKsHUaeb2k+xwNhkWP1c7gP8zjyX/zihx6CFjaQGDng1Kk6PqPmKsZokktX9O0yPGtKiUKBd4NVA1UnsolSKpxcukmzMp09R8
xvjPiO/T9JxxmuBVbudgaH3w2EbSFr9sKRhKXkDocdm/WKmkgbKJn92CgEa9fSZ4chsEEj0iS8KHw0Fsg5/h5VPberJyCXzyW3It+EBjDU8IB5b4zZ4R
dsQzwtY7fUb4AwvA25j1rB969bR/21wdPYZWb6YaCQgrYKAGcf2AmhhNTZSUypEw+g3UVosVPMGK5a8BnCdwT5qohmScZH+1Z4I/tbNZF4Aih0by2GMi
RkuP8CoR2ePyOeMYFvuByy/GFW8aquVxYN2ngWGuZtOt4aXU8glgccOxev4X9T+222JyMUK1DSol0C76jQtdlOokkMZDwITATjOVj/gwrcFpj3mD393p
7QdWo32msYaTW84GaeR+0mNJQqj+zK+AwRYnlXUcW+Mh3yOuYaOYo1cVorZqAMRLeKbI5gf8G6kuCteCXokcwW2PSeilB1Y9t9R5kTWFqsgUMkef4uQk
3sSIZJe0G4IJUo/ZUr4IZjbWkh8zGoT2z0IFqgLgCWS+1Xm5tffortXkndvSTsPJKUDRanhet1o94yvutJUq0RmHcSSFPWq67T9dS7bTA750ku+2t/28
iLrbNJD4E3YNT+t+4rajvm5ty+bO0DxOuo25NL3F0Wml263azfa0mWinL8Q+aMFWh144Qp2CbNpuXH7QGnrx9c+dP/Uk2tsgy0/XjHbjJEY3Ga0/d5qZ
dBsdBbvQuN9H0mG6f+YXvOj+i0bH6hM2ySyUEc11P3GEtNifDtelI7nwBVrvv2bSBjXbKVuL8voXQXlnrDPq6YMztd3P/Hkm+9O15rRl0KMkrTY7CJ/n
Nibazuz5juYvzEzWycBfqIEklIEf0k5rWtbQUW/EzrYuP3Dbtd/H9XljCrs3PXVILpzK1QsynCH18I1G5FT+7SrGJym70Va3N7fCdc9g5+1WQ+qnnqSJ
RG049ZmfO3MNuTM/9dBJjtNqphVm7oUQfw5C/L8P7eWg16aiF87NtuIjBxKWXsTVtt840eyarcmfHxs5+iEtTOg67Zl2C7uhXxpTsCy9jKvtQKsNrEec
UgyShhypbPF0b72TaE+36b5qiwMbSabdyAIboKlYJiz2gVK8PQEs/3nRTKbbwiBRw6mvaQBA4PG23pnvsUY4ohOmxU6Spsi9DVr8eVuj2wZbtrbVQ+Fx
B71wtZ/64inx55lGsx1aazQPKhCWCEw3LhNU09uWzPo2LJuwf+r5qQ++0wiUTSKy5QXkecYZs1E8vptt/OmaBif+nHcBx7Kbc5GcGLmjDaY9Y8BzhUQ/
XYvlxF+YzXaGvCoFqWo45yX3cn5eTNPtaVmTo1ltm/+1JakTQYxzZywVURRQwYDRY8VHzTPN4N+tUCTAgXIj5bLA4WAFSNE85sVGckkT7irzo0kmY58I
xgqH/vKDeiytQLYNI/gSBba6NJnntP2V0lYMS/7CS3F6NNlPog7y/TukySeEC7QbToML8OAAMJts9hDyZ+gA2mtyEgD9ZhMBhliSlZRnWUJOfng3JyAT
6UWbPHvAmQQJSzmtbfFv0mprVeGsyZqpYjsD5xfel8SWmunM07aadDgzg3ksaf6jazZ476GPpMwynFtjCfMXUHyrC+Ga9J/fFUnZ+PkxIgFll6l/wzPS
+jUd+nyMshXsn3Z+/u6nvppuepbCZc0GhOs+KgzpLLjV/LlbMus2yrfIgLbhZMxku+mBT1UC/Wmr2WAqg+ocCH8hhhIcvRvm/IVTT3mpVZc4fy4SRiMI
1pk0gjCYNaqdCpcAZg65vpvRMJZAH4ZV6ZCznS4WxmokQyXa+U6PBD0UlhYHwtXq7AgWLQBaIHeo/o1m0oHUEkzqBDutQjsJk+BU6UbzeNjPjwQLpoof
yEaJSlA7nbNhfx7hhyotNdqah8KG5F3YAcFuRlWwXKp7yYwrEztQ2ddwOvxFlWpIQLcqSsq0yIbMRdHqlW9W5ldm6lYerzxZKa48W5l/3lEDlV4ZWVl6
3vO8Y6Ww8vT5hZVHzztX5o2VWZ7yef55p/hmpfi89/mAsVLEV88HVubw9RP89xTXDKw8Muiilcc0HN8bz/O4fonGzmDUEgR7ijU6cX2Qb0PSJUwEqZ9/
9bwPi/DSmGPQWHlMEhSe92PWxeddNOHSygxNz4I9pfnUF9gtffi8A7/WwM1XrgttGZiroMnQKyg6mqav3AxtkSRfiuXrKzfIAM+qXKlxC3YddDkHrUCX
xsoiaYquh+4Kundkh2ZQhniwsmQ8Pw+RFsgqEPDJ8wvkHDVxe2yuIL1qaeURJoOTPONfipiXvGZOmrmwR54vdV7AGnCkBfxwAbM9hnfwWvhyFvMvYU2o
tFTPRboKAlyI6wisfI9JHsPpBsQ6wwE1L8X2B1a+4U0WIMogx1uJzmW/ADqBo4mWQdgMAbf3DbDfWBmlMMJeBlQPgaLxPMm0sohopNHPu8iAtJDBQUXh
PcPqGuA1vuIYft5DepHBXGRpORT33mpA8Ayt3ALaXF2Z3kPDAdaCwEVW8jyJOAcbkiN1wYQFQgy5z0ekKorQAtt1Hlq5yQ4mQELom3e9wNru5+AqPr9A
U8Kl+TvM3I/POvzvxIyzkIBU1UWWPwDVCNQqGNJUBHPkFiSHANIZxj9yuq7nX5G7EZgCPJ8PavU0Vr4OIihNN7+ywJjF4SKQ+UJclyM8C0kjHP8xSf6U
9KnX5ID2i3DxLt7y8z52Cl+AuHYH26FIOcFveYgZw3O9hdYHAnCATINMSUFA+qQEsvKQhIjohEDYB6zEDvaOJY4YAEB8X4TSLLtg58FgCi0KdwykT0pH
4by7Mr9PKIjCdaDkagIfAQpkVPLPOXI74B6FCnnjSlGjexJUiEpvhcg2Cl3BAbIgsZbij1NyqGboim2tBAsQuSPeK0KEIocmzEO4J4Thel0XyqCcSxY4
/T0USC/AzpcXP8zJeoPKEYaPMtUX/BqmyB5Sij2MPOWI8ZVewwZ7J9ej6ucJrTXLWXFOugUs6sFfBI7rdXC4rMBFc7zWkqizyE0e8L4WlDOVlGwEn/hd
5OgZDJS49kysXS1/ajV5INISgwnNy6WALw9H4vOBmJZPhQKruDKv1/6pVJyRNd9hI2hlpMT5qP5aIo3Iz/q8mnsxnAHJL7ionvWjUpqK5uABGH4W1glm
IdI5ITo7PSbySpig68nU5mUykWQDc2g3mkqzmUhjIgKCmaR662nlImCgEyLN8mUXyuJVrxtVoSTwPHqO6YaHSNKnVhY1+lQrV8h/Cbuknh7H9qyokKRi
IjA8snNVAS0J+Mvrz0rdLKq+2TPmdLpZ0PVTBosilRyiCFyCpUgfj6WmY9pawSl0Gluh3ZVEc1x7a+VbvoJLYngCVW2cV8kes/u4ZoaKCqwijiR8Qv7M
BTWHzPOvSIeC5LIJH0oZHsV0wNjuKt0Wyv2nsF+jK1a2d07p76ZBRtiIXSxyDqAqubqfRbTNykoEIjhAmQdcC1csFKp006omLxHB/SISYjtrK2Mkdh39
wZiIC5/Qj09FDVagvgNlOkkUn0BOlVSK+kQ4ovnGOUfxTxm9zzi9DjBx6NLoxJXVDBHFpIRulRQeKqybxdflFtDr2VW0hagPCn60h+vjasWhRmuv0nKh
ACt6TF+FYGDeKk0/if8z3MRaFImWSTWVL19VpIwVOjJFARYl5L2GliBX3bUQgT1YlKRoP63zZMnW069fjcxvnJ3cXvp649q4Ru9wo9ArL/pmcGsyv9H3
ePPq0tbo463r3ZuX+zaHpzbPD21/tbDRX9jsHMeHXk2xNXVp+34BA14W73Hdsv3s+82v7oiB232TGwszweGbw+c3zo1ApvWOTo3entwCJrvycKP/SUxX
r9K2qzbnNhZmhaixbbzN4cnAyLi2nZAUomzc6NDq0pUodOP+9y+XBgzd50s2+h++fDr86ofu7ftLe+yzbTy+tHVpfOPRw1d95zcvFwJ7jWibbZ79dmOx
I2CWqC7Z1uzV7bm5ciegfXOH7J+L/Zv9l6k/9s/Fs9g6vGPz3ujm/IXtsX6Mg2/RUPF4ibEx2r11sXfr2pONp0PKjfbSx9o62w+/3UMHa/PKrc0HQ9v3
n726ch9ivuo4+2rk0cb5EZL6dsfmzTuvLt3f+u7qga0fr211Pto8O7ExM7R5o+vVtYtGnYF9Ct29XBp7deOHzRvP1B5iGki06q2+Vz9c9UIurlm0/ezq
5uzI5vDZ8BXRzaFXV3u2p59sPb5r/Jux0d/76pubWi2hjdExXIOteA2hzeGOjcLXYrrNqZHNocLb6AeRAeYHgBiwyubAREQHaOPZ062hO/H9ns0LFwGE
/1y8HsRF4AyM97J4zgPIl0/uEIjOLm2N3MeHYnmBRS+LjzcfjL/qO6csHdnAEdcExK/WtxEDyQFH5mN7My+f9Lx8dmPj3tVX3y6p8I5tv2BuBJtwd7Ea
Q7xYEvt/dfXJ5r0fhINvjQ9sPL4ggsID95imyfb0wkZP/8bweGmwc2wrvAjNFtEXkXKeu7X99Knn8bh4o3Bje3ooCG6lEVelq+FNglwhpo1pYmzeu43N
iDX0WhdetnyXzYqNC1eATcKkbOBXHde3n/X5KWnhzkbPwuatC8AuD7W2n323PTIovELpU6NxsPUdRcfGcCGMSdX7BJtzTzbOjQvh9FoCws83zp4HEm8N
D2iwfYGrsQx/8+urm+e645m9FCBUtVQi8RsXv3rV0aFD4AHEm99f3LxxZ/PxRR22LhxRXAVH1CHsQmhdir516eZm/0Xobeve0npHfnv63stHZ6Gf7bt5
VJAb84MbPaEoq0i2BSgiuMTaGsxaDNzue/BuyDR06gUFYCrgXxHcWdajF+6+LHYopK5CjiWC3R+EbmMpcMW6EQlq66sCIBj+rPRfkb8CDgGKGiT11d3L
wGiRjkSm++ci5Lsss8/Q3MvipIjOAE5H0k65S+HSKpUELq7OIMWVwgnE9gWKBC6uQhNRj289GRZ6ETmMCzUxiQThbwY9ZYoSVIP3RdcEkZqiyc+YUXRu
veuH9a7F9a6R9a4H613965131zvH1rsur3dOr3dNrnd1rXd9jQEa9I4q3/Wu6zRbZ3E9P77edWG9c3S9a4L/7F/PD613Dq7nb6539a533lvvfMTLfePp
Y73rK5akn1bHuhAg/yON7Lq93nVlvStPP+SvrOeX1vPXsLutgSlwv/X8fX+Gznme4SL9mf8R+t56eD14iQYbLNUHieBp4koMOYzUXnXux3JuFrHYs3ii
KDcllHl3Pd/Nu/tuvfNbLepYtr/7uq8o8LU8StfCfp3P5A+wgS6XZFPCph2kos6F9a4xFuQKqavzCWsMDvBkDxTzVX4CFJPNfXej97xS1LX1zoE4olkW
BN+8ujywnr8QSzore959dUsGm+bmen7af6sB5t6eRYSce/nk6nr+a8811/Pf4kJ2/Ql2o4c0j2Sj6/lBWkEqXvz5zUbfXVDUMvfWZqklPkpsVW4A4ctm
2QNtxV7lTsg5LnP4/MCmfsKOMraeP+85LG2b4huKG+Y/zx5Q2+zfHBjZWJzfnLi53fUU022OT6B8xK63fni8PXlemHU9P4C6QF1y1uCNnF/v+lEZ8HxY
NTHkd6t75NXIIFtBIlEc+eXpb/nARZvHls6FQzKaC2/NdQsWDOVsTwIX80JoLUaMixUVxgx0ef7ZxgXAz7m3wYNf5Yub576Hc26ODiNDKVBaYp+8GUGL
oentu5cDsRlJjte7vhFxedDPL8iAbKhQKinJONsj48iYAhDgJ6hX4Tzr+Ukh9tZ098Z3s3zVCMV/XkJHEO7LgiuSVIevvC+WiWTXKuhV0oQwlVaPJd58
yQPOmPfhAZgnUO3GMnCW4luOzenw+qFo5ZKmVOL8dDhQixz6gYgn/V5mxBcZ/UogaV3T5fA8+noliBzzkz9BTRgj5WIXKEbhm1hVk+VXKEVoHwKqzpai
GP1KDk9tAD/viLWfVZEgpitQqRYaDAhQHmgRDQMWCFRCr1vAPQKjrHzihd5N56D/IUG/Xz5K32N/9IvHbtbNjyoHy8RbOTm8fDKgEewanYaN+9+BuJRb
K6bfICOE0Os2Z8lJb1e1tB8EgG6chS/+qPJiqctrdCYoVYsSvevH+P7EueGt2YH4/kR5HR2ApEqNis3+JyobxDQq1jufsq4IXATDg3PrdCy8wfq9isDg
uHYFq7FAiab7wsbXD6houdu5fTdPJUr+3HondPBg4+YAEvz20qJAadHhKHO8qs0MlaoGlf3LcSC6t+G3NAzPIqWzvMVeBzuJ5yFj23cHlAtE9Tr670Qm
Ta+oqNIA2bg/+PJxbzUQj22JRNGgH+lMIH9JyPDq+nf0ydAFLioHt649hrmDcBrZO+EN9YtaRqODQgch/TcoB4ZLIlWiBcplpbiNnvGXT7+tVnPoNlrK
SxgRZRFJMr4HI/1clTEac1VpyTDiC0o5zQJShSRMIjo0MseNB7syRoBmVKsgojs20WXqxuLC9uQzVZ3uzTIkQ6MT1djZmRzfmbj6pveWsfvdI+NNfm7n
7tSb8zqNHB7/1dXdG3O7Yx271zqM14Wh3ZGC8ebr4d2B4Z2Lw8abnvO750bfnCvu3Fn0NLfTU6RPHuZ3r1w0dh+P7M5efTN0dffKlLH7/cXdWf9odrd7
eGe2m0ddKbyeLbwZmtgZ6N8ZGNV5Eme3f/h1gS/mLT2N6ciU771qZ0UIs3NxPLYHo0ZehX6M3aFzuzcGtXovvkl0ey6vFzpIhzeu+sq7USTVd8/hT+2m
y5tL3Tsjwzvnh3Z7rhr0y92p3Z6BPR7l797K716fgFivH9wyYDX4hGfIq7vdxd3rl+L6LQHfvDL3uljQ7rNI14E+uMPy+52rF3fOXfK6K7s3uo2dh9fg
czvdt3fPzUu3gtLmjJ3i4M6Nxd3rhd1bHTTB7rNbbzpH6Wqo5vVCkRSLy+G15Cxhl9zLyT/Jeau79hYKSUTyvum6xyFy6+vXC4tvhqCqfgh9pTewLZL/
wdzOt8MHdjuncA3Cz3hzbWj3+zvG64eDb4aGaUP0wY0imWumsNOJKYojOxOYfnB8p/si7/jK0+BeY5oku7eGdyYnSBBWHcd8XJtkd6h7dywvB78udIS3
ofUwyc5DCHrjqfKVuL7IzmDHzvfz3oMj8mqK07fQFtntvvWm64bxZugeNEWmfF24CKQgvLjSX7Lvyv2Rnak5RBVshnHx/ZE3g/mDAcCGb4fwOYTeCpFv
DcEv4CFGibCIn4lLFJ4jwwTbO3d7gtJGtj8EHssJI9se7N5K3iCOR/U4dsbyb/JTO2ODBgXw95p3F2Cz8BnAhlymciCFBCK/llEkcBPgsPP9OIB+hNDt
3CgpdbcwvjMwXpq5YtoYO+OLkL0cja5cJAFl/FPQe4jkIareUxt+CiZDAvG/Ha684RLYhjwP5oILxbQnAuvMyXUqenf1poRY2dgd/bqGroRfMlCWfifN
CMAfooZrIM+Bg32InQvdr+enDEQQwMaQ6akMct/0zpPSlUuGnEij6wB02+3K+wVXTLfhzbXh3e4ZX2C9FoOIMsobRdjpFpdcr2c7kLTDaT66tyDSRWxX
YacwvLswGN9V8AoHkRo9HKjUUNj5ZnR38ikG6vQUdnsHqaBCotjtvkFuqdNP8EfrNBR2b/XvDj3V7Sa86SII2AewuLozXTSgbVxs7BQuAJa7d2Zv7Vxi
tN7tugHblrhQ1f6BwHbOwKy7ikGr20HwTVAtGn+jroEPlASbQC5VFUQ0DbC7nTvFQNBScrvjXVmlVbB7rXvn9mBFCNXuEwQL9iDnucr10/mCsTNVRJSQ
USgKum68udxfVhxVaBBICJfgrQqF6C6BTECh8kDk60B9iFCfDaZDGlNWCcS1BcIZFVOIWKmuysh+gKxJSBKJShHTVGkF7HTTjne/G0QVIXVu1ElrcFwF
OgChujmYF6Npv34Npq9rxi43iuiv9az147/e1cm1fgM/nlsdN9a61rrph9UZDb6/OrbWjYs7VxeM1fm1/Oq91bHVSYM+VbP18y+9q/Pi0x7879zqrLGW
XztnrE7jElywOmPg4278eG+1yDMohfL89/HR/OqEsTq+1gW988yTWK4bn/CX02s9Fa7g8Tp9AayNDaw+MlbvQMKzRlArMT0CvnQ+sOHoPsFajxA8tk2w
+hiTkVYC4+NaBKuP1npZ3b3ciWJlh/cR1zKoMMPqj2vn1rqraVerg7B6Z/XR6jSZifbzANd10tTd0DZEw+dTe+wm4OIFzHaO5uvnVbAG+ckY+xI0Rz/F
NRQwJo+x98rNHtVV4DXmsXKJzsIK6lB3c6zeRoBxq0F+HeWyBsSZpN1QUHBkiDAx+NMZbDiv9jiDj8bx94KKrv7VKfxHWuAIW+tbLayOyS/Zl/K8U3L2
Sdhiiq3Lk02v3sf+u/feteBZ7kDpZOuZGroXtBMD/v6ADIq/H4U0g+3Mk2AqOqHwblKNMjQUoLAGm13rY/XdW+skr4A1jdXb+LQfZiUNMszcEw7YD7SZ
UHpjBWOWeweUIhHYj7g+Z9t5XsJfBy9enSLjkx+uTmi1QRj1CPNwDe3c96C4ZogYPklGRjBVR0/8MEmaIul+kNPGvnID03VSLJHrdMKxxlZHVie0Oiac
AGY4Z4z5L9yoPN9b6KFAnB5hpq7VOQqVR+QFwlXuQapzq9MR/RPOLWTSPgkdkf0TSmzkNasTB7FLWneclse+KcmVpj4ZnOzCyj4ia06QQ6pM4sspI1Mo
kjKzb+cJjbYKht5nrx4jbxMycKaLbLGwHCKWQiLGtlkQWPnVoriG7D0OWAIKYUW9hovEAQjZJS0mRaHg5VCsIBVHO/QxUxksBCY95PBFaOg1Wyj3wlRn
q2Gkml3BVATUEngwXg3otWEY4gu0GwKxHrG0H+jSiIFg30/RPialqpICtRozImw0V41p0VAAARHOl8ih169RRY9IoO/wuRM/NnsoAlmxdV6YK/eEzsdJ
R3MMgQuUb6YJzoNVrMwgcYlDo3/jJw/h7VNwu4mYJo6/j15IuSDHx3ZxpCdTJE9hkwzj4yoSONQ1mjglyS2qkcPgRBVA3vOwyIaOgpsqRXulvg7CF7WU
TlOHA6Hbi3zogeqaGZ3WTtj5vXl0+jwkPIHJ6oxur4eL6DlKrmI5VRxxsdPvBe59lVqm+ftuwqt+Kd9DDvheHx6r9oAERPgoH0xHGv0fLzHLKRg2382T
MlRHrz6UwF3qORG9IBHxXuq5DcXOrJ2N6gOVJLU7lMRZd/GvZ6UqYPVHA2ssrN6mon2MVvyRTTVSMd2V8rWIHhBmeiQLDFwlU4g0Y3QniMWaDhQ3Xopb
HRRQJyHSL2g8hY3je65iAFtjeu2gEgX6cVmhGNDoB5VMJz3Rd+TJtdh3OkBfc9QlUSSLJZgj6sYJooR+eNlvrYtiLpou1vBKh3+p3KzdUsLFNf/dUEUL
dTpIftsIwlLDiLse0A8t/PdBMszfB4nUzAG8YO4ZCoNpIj2kyEmSvv+XCTnaT7n3xdYfchX6y8RaLyaZFnxunKZ5gMDtxyzhC6gfQUurEmStB3PC2vhK
p5OEdboh8pJYg4ButSgDPfKfHQ3pK7Z7xKqIbSH5e6CtP5AZNbaJ9APpdvWBJ7xBatV9nSvZhNC0d81/OhfaA3JpdYrWliDpHDHc3tUi9NjDZJdUCtPt
sUdEu7nP9RhU4DsCFWRUpo1zru4zGEm72dT0ve4/VSq8kSaZ9eaO7R0F/qEWjB8TfaEqDilXmCIr/DJNDGN1Bi6p1tpPP8H9/z74j45BGQIcNezDPOEv
HCv9/Mu0cn7hEaRgmp5mo6JSOjymok/w2fQeu0F/axFjv6z1GSDyH9rCpMBIT2CxJSq0f2EQmOZm0QxtxGCvmVbbgEqwcwK+WU8fQJhZ6ZUCbvCDCNXV
6QOYSQKMtOWMnNfzlTqDzbS2RCKRk8jCneauYP+amkC/sFDj5Iu0Qf1WUB+PnC1FwWCsx7R8DNHzgWY7KQB6UGWICrIv7Mp67R8MhxpC3R85swg/Mg22
9Tb6P2PszZRVxymgezn2YWOG8ujmD42dpp2TEhA4ZCRpjJg+0C8TGNl3UCUvP015HseuNcVx52uYqoUx+NcvzAkDckv9Sf/3qwZyRaq/8Occ251Vz0ER
SFMxbaHAQiplRbaEVPx5W5GoIZJdGPgiGkSiGWTwZb20Y1ZSYfU+rp4pdbvYdpHQLktGEVmKHUH5sBCFcdk2gjBCmiT7I4H1GSXQQa6rop3PMKYZTHk9
iTp6jaVuQW/XBgzuj00ybnHgVEHtMRY9kEv7fYiJ6yUJaACaC3CQGULAFRYgpRN8En8EamJXj4KoF5UetRpK5fVZ6dJc0vVKe0d0k9jQRFG6jVqbSRw+
4rmkcPn0DjtKAm3vSScit6TifYFUQqyhV5hdeMgs/6jsXzUPlcVizQ0lTkQl4CJON1entfpLtKs8n1p16/WXvHYS7d4HgQoZ+NftMyG0ZgVkavSYAnJN
+4ao3mDiCoWUp9lkmiJZOMAktSXGw8wRUz3SajcFo6PKhFq9p+A83ra1+1B+oc466KWDXg56opr3mV7PC8d/RChL2RjfkJRTIj+T3IbfFqjahCpNgAEL
zXnFd2QbijYaeJqpdIa32IaiTgIdNa1OV3KwiD6UKBj8PMbkYCYqL1dpT61OYlSPcLpeT4FRXSmqPLHYmCAPDziSeiXhrcb+yOxzQi5Dkbwf8Nl0VIMq
mH+4pznJ3tkb3ldMr8rPsqHCrKQC4ungjoyvAYogj+Arq1WzfVVSlVQspaoCgShdNNpapVEh1B5wbZnmNVpbtFGKhHH80CfSk0y5JFzg3yIKU9Ry2Nbq
YtVYLrOFhIRhDdLoOSpa+uTStPZpO6ph9WL0/os7A/xn34vRzhejN/Gr8WJ0nn+fwk9DL+6cfzE6+2L02YvRyRejBY1+1ovRHh47jkmMaiuM86c3sciL
O3ksY3hDQstP8k/D+OkSFsdfj16MLr0Y7WORxA83WUqS7wJ+uiL+usnXCeGL/KcYxmvjr36eGh9fF9NigoJcvg6/TmMh/NXL+xgS+9DYVJFnOU8b4FmL
YvAwb3Hkxejii9FRiM3bgAh3RjUaatXWmlSCB+wT2khMzy3WyFXbaqEN8QYf8e8XYhty5VeO88UdwuysFCl4XJeumlqmX9zpZMMVIBD2pNW38y3Nxqk8
s/hFOpP6rlhtybhGn/K/Aq81omzwECoxAt91CCci991b948FE5a+K+YOuLoXVZNsFhijSwwYD4TPzXKvimgLVtPeZf50NjBJVH9wT9HG1pNdRQ7hJf/F
QRzozzwjs6HyCEBhV7LilFQ5fXmWHJWitRCCpSEBVWwV+l7IIFBovE785aNMkWW9xwNmKfDvdPg+rmI/sq9YOzrTTWhqZeGpbLnR2zXcixbh/0WBrP1i
8VEB26EQnlRggOuuCyUMCdDbbyjVTfH0wnQjBwI6JE+74KO2H2UyKnrYIfIkGk35TBhZLDwsfiI7h2B8KWikEeHq/Sz0bKlXKpvE9C1JHNqG2MCCUEKB
MxMkGq2Q2+I6mbyze8I/pFKHeIr+yonS17jefW2ssGd83U1hox8EAFeaoVKtzS52hZ35CgWNanFWmXh06W00OTmkpz1MWxICyPx6XRQIEY1O6U5ARdLD
ZXGZiJZxdWVkw5OBP6/clfzxoFeJFIUxhQ93CKsFpQ2WQIZXYRQ4kUyWYk55fle4KK+b9aofOHcX+SRdc8svEiKroaoVVSEcFpEN1XJB5XYjm6oeqJTK
7RdqUcVNVKNVKIdzw6ynok5hiSLHTx9XI95ksc1WURV6gFMqWwgGhzx0ktAUgsJgiekXuFHIF/jujlSZVNGUFOPOWQUc5B+VqsyYxizP2MUuM+1F0yMB
jjVlycrZw//eT5lGIGWWeltcZ7ciMM6KYqpCXTmgDMCf7dctefxAkxqRuxd5pxiSXKsjHCN2mMfENIUrii33KvKR8oH9+/drv7vKYy2VdeK79RXx07tp
IwdsISqBKXa2MS8HhxGlrqpf3mTMneUJloQrx1YNQ6oqpQFjvASuuR9UXTXcUu6t0ZZmDXfy/ubFGtVKi+p9aeX0QlPPxBwh1eg1qkOYqYJCbjYEE0XW
cIdadCGQVn2oje5jl9dXsf1szuWElvHtbC18Ks8OlTrdrEkI+h3G6fS6Ve4VlG/Uh5V5Ns1DheR9vOvbOp3vatdKnZcCAbabV1gQ1QgPmzuQwyrDSlxn
XJGGK/uEep+F1E+/DnnxVlB1/EOfnV7g8efZ6fuYLItcfoXrZZp52gtev4ILlvMV2+jBCqx0vwrcanopWBUvikXL36i1Hu/qqvb1/Tyi4c7DOxXt7gvV
Lnr1WpUGPE/UKUwflbqlOaI68yUtuxobOZJ7eRw2z1Ws34MItQG+E0YVXb0p1dPq8LphXuFVsbXv1R8H48oKjRZ/CQZXrvavy3qHJ91nBKq8gO5DFo1J
YXFt/4jiPgL19CvpJY0zgZLGXvV4D61ZZZkqBwZKqTdFI0p0XVQqlAaYVlW/amuptBissGStf8krqDXamxU6H8o+0acOvw1XrOBXCilq43e8iS+//H/O
wOW7jPkAAA==
`
function loadStrings () {
const buf = Buffer.from(PACK_B64.replace(/\s+/g, ''), 'base64')
return JSON.parse(zlib.gunzipSync(buf).toString('utf8'))
}
function main () {
const STRINGS = loadStrings()
const payload = {
version: VERSION,
strings: STRINGS,
}
fs.writeFileSync(
path.join(__dirname, 'translations.json'),
JSON.stringify(payload, null, 2) + "\n",
'utf8'
)
console.log('Wrote', path.join(__dirname, 'translations.json'))
}
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "0.3.1",
"version": "0.4.0",
"description": "__MSG_extDescription__",
"default_locale": "en",
"permissions": [

View File

@@ -3,6 +3,12 @@ const ALLOW_INSTANCE_CONFIG = false
const DEFAULT_BASE = 'https://memento-note.com'
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
/** Compatibilité Chrome/Firefox : Firefox expose l'API sous `browser.*`,
* Chrome sous `chrome.*`. On normalise pour utiliser `chrome.*` partout dans le code. */
if (typeof chrome === 'undefined' && typeof browser !== 'undefined') {
globalThis.chrome = browser
}
let state = 'idle'
let notebooks = []
let selectedNotebookId = ''

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test Sidepanel</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.error { color: red; }
.success { color: green; }
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Test Extension Memento</h1>
<div id="results"></div>
<script>
const results = document.getElementById('results');
function log(message, type = 'info') {
const div = document.createElement('div');
div.className = type;
div.textContent = message;
results.appendChild(div);
}
function testFile(filename) {
return fetch(filename)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.text();
})
.then(content => {
log(`${filename} chargé (${content.length} bytes)`, 'success');
return { filename, content };
})
.catch(error => {
log(`${filename}: ${error.message}`, 'error');
return { filename, error };
});
}
async function testSidepanelJS() {
const script = document.createElement('script');
script.src = 'sidepanel.js';
script.onerror = () => log('✗ sidepanel.js: Erreur de chargement', 'error');
script.onload = () => {
log('✓ sidepanel.js chargé', 'success');
// Check for CSP violations
if (typeof chrome !== 'undefined') {
log('✓ API chrome disponible', 'success');
} else {
log('✗ API chrome non disponible (normal hors extension)', 'error');
}
};
document.head.appendChild(script);
}
async function runTests() {
log('🧪 Début des tests...', 'info');
// Test file loading
await Promise.all([
testFile('sidepanel.html'),
testFile('sidepanel.js'),
testFile('content.js'),
testFile('background.js'),
testFile('manifest.json')
]);
// Test sidepanel.js execution
await testSidepanelJS();
}
runTests();
</script>
</body>
</html>

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "مومنتو ويب كليبر"
},
"extDescription": {
"message": "التقط صفحات الويب والنص المميز في دفاتر ملاحظات Memento الخاصة بك - ويتصل بخادم Memento الخاص بك."
},
"extActionTitle": {
"message": "مقطع إلى مومنتو"
},
"webClipper": {
"message": "مقص الويب"
},
"connected": {
"message": "متصل"
},
"disconnected": {
"message": "غير متصل"
},
"instanceSettings": {
"message": "عنوان URL لمومنتو"
},
"instanceUrlLabel": {
"message": "عنوان URL لمثيل Memento الخاص بك"
},
"presetProduction": {
"message": "إعداد مسبق للإنتاج · memento-note.com"
},
"applyReconnect": {
"message": "تطبيق وإعادة الاتصال"
},
"openMomento": {
"message": "افتح مومنتو"
},
"settingsHint": {
"message": "الصق عنوان URL الخاص بـ HTTPS (أو LAN) لخادم Memento الخاص بك. تتعامل ملفات تعريف الارتباط الموجودة في هذا المتصفح مع تسجيل الدخول."
},
"footerVersion": {
"message": "Memento Web Clipper <<<الإصدار>>>"
},
"errPermissionDenied": {
"message": "لا يستطيع Memento الوصول إلى علامة التبويب هذه. تحقق من أذونات ملحق لوحة المفاتيح/الموقع — أو افتح اللوحة الجانبية."
},
"notebookUnnamed": {
"message": "دفتر بلا عنوان"
},
"noNotebooks": {
"message": "لا توجد دفاتر ملاحظات حتى الآن"
},
"readingTimeOne": {
"message": "~1 دقيقة قراءة"
},
"readingTimeOther": {
"message": "تقريبا. $COUNT$ دقيقة قراءة",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "تم الكشف عن التحديد"
},
"ignore": {
"message": "يتجاهل"
},
"selectionHint": {
"message": "نصيحة: قم بتمييز النص الموجود على الصفحة لقص التحديد الدقيق كملاحظة."
},
"clipSelection": {
"message": "اختيار المقطع"
},
"clipPage": {
"message": "قص هذه الصفحة"
},
"saveLinkOnly": {
"message": "حفظ الرابط فقط"
},
"pageNotAccessible": {
"message": "لا يمكن القص هنا — هذه الصفحة تحظر الوصول إلى الإضافات."
},
"errLoginRequired": {
"message": "يرجى تسجيل الدخول إلى Memento في هذا المتصفح أولاً."
},
"errLoadNotebooks": {
"message": "تعذر تحميل دفاتر الملاحظات. حاول إعادة الاتصال."
},
"notebooksLoaded": {
"message": "تم تحميل دفاتر الملاحظات"
},
"connecting": {
"message": "جارٍ الاتصال…"
},
"connectedToUrl": {
"message": "متصل بـ $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "صفحة مقيدة - قم بالقص عبر شريط أدوات Memento أو اللوحة الجانبية."
},
"destinationNotebook": {
"message": "دفتر الوجهة"
},
"activePage": {
"message": "صفحة نشطة"
},
"previewBeforeSave": {
"message": "المراجعة قبل الحفظ"
},
"noteTitleLabel": {
"message": "عنوان"
},
"excerptLabel": {
"message": "مقتطفات"
},
"saveToMomento": {
"message": "حفظ إلى مومنتو"
},
"back": {
"message": "خلف"
},
"analyzingSource": {
"message": "تحليل المصدر"
},
"statusAnalyzing": {
"message": "جارٍ التحليل…"
},
"statusSaving": {
"message": "توفير…"
},
"processingDetail": {
"message": "إنشاء العلامات والملخص الدلالي والتضمينات."
},
"noteSaved": {
"message": "تم حفظ الملاحظة"
},
"sentToNotebook": {
"message": "تم الحفظ في $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "عرض في مومنتو"
},
"clipAnother": {
"message": "قص صفحة أخرى"
},
"failure": {
"message": "لا يمكن إكماله"
},
"genericError": {
"message": "حدث خطأ ما أثناء الوصول إلى مثيل Memento."
},
"retry": {
"message": "أعد المحاولة"
},
"errNoSelection": {
"message": "حدد النص أولاً، أو قم بقص الصفحة بأكملها."
},
"errAnalyzeFailed": {
"message": "لا يمكن تحليل هذه الصفحة."
},
"errSaveFailed": {
"message": "لا يمكن حفظ ملاحظتك."
},
"errNetwork": {
"message": "مشكلة في الشبكة - تحقق من اتصالك وعنوان URL الخاص بـ Memento."
},
"bannerPickText": {
"message": "قم بتمييز النص الموجود على الصفحة، أو قم بقص الصفحة بأكملها."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Web Clipper"
},
"extDescription": {
"message": "Erfassen Sie Webseiten und hervorgehobenen Text in Ihren Memento-Notizbüchern stellt eine Verbindung zu Ihrem eigenen Memento-Server her."
},
"extActionTitle": {
"message": "Clip auf Memento"
},
"webClipper": {
"message": "Web Clipper"
},
"connected": {
"message": "Verbunden"
},
"disconnected": {
"message": "Nicht verbunden"
},
"instanceSettings": {
"message": "Memento-URL"
},
"instanceUrlLabel": {
"message": "Ihre Memento-Instanz-URL"
},
"presetProduction": {
"message": "Produktionsvoreinstellung · memento-note.com"
},
"applyReconnect": {
"message": "Anwenden und erneut verbinden"
},
"openMomento": {
"message": "Öffnen Sie Memento"
},
"settingsHint": {
"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.0"
},
"errPermissionDenied": {
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen oder öffnen Sie den Seitenbereich."
},
"notebookUnnamed": {
"message": "Notizbuch ohne Titel"
},
"noNotebooks": {
"message": "Noch keine Notizbücher"
},
"readingTimeOne": {
"message": "~1 Minute gelesen"
},
"readingTimeOther": {
"message": "Ca. $COUNT$ min. gelesen",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Auswahl erkannt"
},
"ignore": {
"message": "ignorieren"
},
"selectionHint": {
"message": "Tipp: Markieren Sie Text auf der Seite, um eine präzise Auswahl als Notiz auszuschneiden."
},
"clipSelection": {
"message": "Clip-Auswahl"
},
"clipPage": {
"message": "Clip diese Seite aus"
},
"saveLinkOnly": {
"message": "Nur Link speichern"
},
"pageNotAccessible": {
"message": "Hier kann kein Clip erstellt werden diese Seite blockiert den Zugriff auf die Erweiterung."
},
"errLoginRequired": {
"message": "Bitte melden Sie sich zunächst in diesem Browser bei Memento an."
},
"errLoadNotebooks": {
"message": "Notebooks konnten nicht geladen werden. Versuchen Sie, die Verbindung wiederherzustellen."
},
"notebooksLoaded": {
"message": "Notizbücher geladen"
},
"connecting": {
"message": "Verbinden…"
},
"connectedToUrl": {
"message": "Verbunden mit $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Eingeschränkte Seite Ausschneiden über die Memento-Symbolleiste oder den Seitenbereich."
},
"destinationNotebook": {
"message": "Zielnotizbuch"
},
"activePage": {
"message": "Aktive Seite"
},
"previewBeforeSave": {
"message": "Vor dem Speichern überprüfen"
},
"noteTitleLabel": {
"message": "Titel"
},
"excerptLabel": {
"message": "Auszug"
},
"saveToMomento": {
"message": "In Memento speichern"
},
"back": {
"message": "Zurück"
},
"analyzingSource": {
"message": "Quelle analysieren"
},
"statusAnalyzing": {
"message": "Analysieren…"
},
"statusSaving": {
"message": "Sparen…"
},
"processingDetail": {
"message": "Generieren von Tags, einer semantischen Zusammenfassung und Einbettungen."
},
"noteSaved": {
"message": "Notiz gespeichert"
},
"sentToNotebook": {
"message": "Gespeichert unter $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "In Memento ansehen"
},
"clipAnother": {
"message": "Schneiden Sie eine weitere Seite aus"
},
"failure": {
"message": "Konnte nicht abgeschlossen werden"
},
"genericError": {
"message": "Beim Erreichen Ihrer Memento-Instanz ist ein Fehler aufgetreten."
},
"retry": {
"message": "Wiederholen"
},
"errNoSelection": {
"message": "Wählen Sie zuerst den Text aus oder schneiden Sie die gesamte Seite aus."
},
"errAnalyzeFailed": {
"message": "Diese Seite konnte nicht analysiert werden."
},
"errSaveFailed": {
"message": "Ihre Notiz konnte nicht gespeichert werden."
},
"errNetwork": {
"message": "Netzwerkproblem überprüfen Sie Ihre Verbindung und Memento-URL."
},
"bannerPickText": {
"message": "Markieren Sie Text auf der Seite oder schneiden Sie die gesamte Seite aus."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Web Clipper"
},
"extDescription": {
"message": "Capture web pages and highlighted text into your Memento notebooks — connects to your own Memento server."
},
"extActionTitle": {
"message": "Clip to Memento"
},
"webClipper": {
"message": "Web Clipper"
},
"connected": {
"message": "Connected"
},
"disconnected": {
"message": "Not connected"
},
"instanceSettings": {
"message": "Memento URL"
},
"instanceUrlLabel": {
"message": "Your Memento instance URL"
},
"presetProduction": {
"message": "Production preset · memento-note.com"
},
"applyReconnect": {
"message": "Apply and reconnect"
},
"openMomento": {
"message": "Open Memento"
},
"settingsHint": {
"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.0"
},
"errPermissionDenied": {
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
},
"notebookUnnamed": {
"message": "Untitled notebook"
},
"noNotebooks": {
"message": "No notebooks yet"
},
"readingTimeOne": {
"message": "~1 minute read"
},
"readingTimeOther": {
"message": "Approx. $COUNT$ min read",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Selection detected"
},
"ignore": {
"message": "ignore"
},
"selectionHint": {
"message": "Tip: highlight text on the page to clip a precise selection as a note."
},
"clipSelection": {
"message": "Clip selection"
},
"clipPage": {
"message": "Clip this page"
},
"saveLinkOnly": {
"message": "Save link only"
},
"pageNotAccessible": {
"message": "Can't clip here — this page blocks extension access."
},
"errLoginRequired": {
"message": "Please sign in to Memento in this browser first."
},
"errLoadNotebooks": {
"message": "Could not load notebooks. Try reconnecting."
},
"notebooksLoaded": {
"message": "Notebooks loaded"
},
"connecting": {
"message": "Connecting…"
},
"connectedToUrl": {
"message": "Connected to $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Restricted page — clip via the Memento toolbar or Side Panel."
},
"destinationNotebook": {
"message": "Destination notebook"
},
"activePage": {
"message": "Active page"
},
"previewBeforeSave": {
"message": "Review before saving"
},
"noteTitleLabel": {
"message": "Title"
},
"excerptLabel": {
"message": "Excerpt"
},
"saveToMomento": {
"message": "Save to Memento"
},
"back": {
"message": "Back"
},
"analyzingSource": {
"message": "Analyzing source"
},
"statusAnalyzing": {
"message": "Analyzing…"
},
"statusSaving": {
"message": "Saving…"
},
"processingDetail": {
"message": "Generating tags, a semantic summary, and embeddings."
},
"noteSaved": {
"message": "Note saved"
},
"sentToNotebook": {
"message": "Saved to $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "View in Memento"
},
"clipAnother": {
"message": "Clip another page"
},
"failure": {
"message": "Could not complete"
},
"genericError": {
"message": "Something went wrong reaching your Memento instance."
},
"retry": {
"message": "Retry"
},
"errNoSelection": {
"message": "Select text first, or clip the full page."
},
"errAnalyzeFailed": {
"message": "Could not analyze this page."
},
"errSaveFailed": {
"message": "Could not save your note."
},
"errNetwork": {
"message": "Network issue — check your connection and Memento URL."
},
"bannerPickText": {
"message": "Highlight text on the page, or clip the whole page."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Cortadora web Memento"
},
"extDescription": {
"message": "Capture páginas web y texto resaltado en sus cuadernos Memento: se conecta a su propio servidor Memento."
},
"extActionTitle": {
"message": "Clip al momento"
},
"webClipper": {
"message": "Cortadora web"
},
"connected": {
"message": "Conectado"
},
"disconnected": {
"message": "No conectado"
},
"instanceSettings": {
"message": "URL del momento"
},
"instanceUrlLabel": {
"message": "La URL de tu instancia de Memento"
},
"presetProduction": {
"message": "Preajuste de producción · memento-note.com"
},
"applyReconnect": {
"message": "Aplicar y reconectar"
},
"openMomento": {
"message": "Memento abierto"
},
"settingsHint": {
"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>>>"
},
"errPermissionDenied": {
"message": "Memento no puede acceder a esta pestaña. Verifique los permisos de extensión del sitio/teclado o abra el Panel lateral."
},
"notebookUnnamed": {
"message": "Cuaderno sin título"
},
"noNotebooks": {
"message": "Aún no hay cuadernos"
},
"readingTimeOne": {
"message": "~1 minuto de lectura"
},
"readingTimeOther": {
"message": "Aprox. <<<CONTAR>>> lectura mínima (Approx. $COUNT$ min read)",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Selección detectada"
},
"ignore": {
"message": "ignorar"
},
"selectionHint": {
"message": "Consejo: resalte el texto en la página para recortar una selección precisa como nota."
},
"clipSelection": {
"message": "Selección de clips"
},
"clipPage": {
"message": "Recortar esta página"
},
"saveLinkOnly": {
"message": "Guardar enlace solamente"
},
"pageNotAccessible": {
"message": "No se puede recortar aquí: esta página bloquea el acceso a la extensión."
},
"errLoginRequired": {
"message": "Primero inicie sesión en Memento en este navegador."
},
"errLoadNotebooks": {
"message": "No se pudieron cargar los cuadernos. Intente volver a conectarse."
},
"notebooksLoaded": {
"message": "Cuadernos cargados"
},
"connecting": {
"message": "Conectando…"
},
"connectedToUrl": {
"message": "Conectado a $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Página restringida: recorte mediante la barra de herramientas de Memento o el panel lateral."
},
"destinationNotebook": {
"message": "Cuaderno de destino"
},
"activePage": {
"message": "Página activa"
},
"previewBeforeSave": {
"message": "Revisar antes de guardar"
},
"noteTitleLabel": {
"message": "Título"
},
"excerptLabel": {
"message": "Extracto"
},
"saveToMomento": {
"message": "Guardar en momento"
},
"back": {
"message": "Atrás"
},
"analyzingSource": {
"message": "Analizando fuente"
},
"statusAnalyzing": {
"message": "Analizando…"
},
"statusSaving": {
"message": "Ahorro…"
},
"processingDetail": {
"message": "Generación de etiquetas, resumen semántico e incrustaciones."
},
"noteSaved": {
"message": "Nota guardada"
},
"sentToNotebook": {
"message": "Guardado en $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Ver en momento"
},
"clipAnother": {
"message": "Recortar otra página"
},
"failure": {
"message": "No se pudo completar"
},
"genericError": {
"message": "Algo salió mal al llegar a tu instancia de Memento."
},
"retry": {
"message": "Rever"
},
"errNoSelection": {
"message": "Seleccione el texto primero o recorte la página completa."
},
"errAnalyzeFailed": {
"message": "No se pudo analizar esta página."
},
"errSaveFailed": {
"message": "No se pudo guardar tu nota."
},
"errNetwork": {
"message": "Problema de red: verifique su conexión y la URL de Memento."
},
"bannerPickText": {
"message": "Resalte el texto de la página o recorte toda la página."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Web Clipper"
},
"extDescription": {
"message": "صفحات وب و متن هایلایت شده را در نوت بوک های Memento خود ضبط کنید — به سرور Memento خودتان متصل می شود."
},
"extActionTitle": {
"message": "کلیپ به لحظه"
},
"webClipper": {
"message": "Web Clipper"
},
"connected": {
"message": "متصل شد"
},
"disconnected": {
"message": "متصل نیست"
},
"instanceSettings": {
"message": "آدرس لحظه ای"
},
"instanceUrlLabel": {
"message": "URL نمونه Memento شما"
},
"presetProduction": {
"message": "پیش تنظیم تولید · memento-note.com"
},
"applyReconnect": {
"message": "درخواست کنید و دوباره وصل شوید"
},
"openMomento": {
"message": "Memento را باز کنید"
},
"settingsHint": {
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکی‌های این مرورگر ورود به سیستم را کنترل می‌کنند."
},
"footerVersion": {
"message": "Memento Web Clipper 0.4.0"
},
"errPermissionDenied": {
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
},
"notebookUnnamed": {
"message": "دفترچه بدون عنوان"
},
"noNotebooks": {
"message": "هنوز نوت بوک نیست"
},
"readingTimeOne": {
"message": "~ 1 دقیقه مطالعه کنید"
},
"readingTimeOther": {
"message": "تقریبا $COUNT$ دقیقه خواندن",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "انتخاب شناسایی شد"
},
"ignore": {
"message": "نادیده گرفتن"
},
"selectionHint": {
"message": "نکته: متن را در صفحه برجسته کنید تا یک انتخاب دقیق به عنوان یادداشت بریده شود."
},
"clipSelection": {
"message": "انتخاب کلیپ"
},
"clipPage": {
"message": "این صفحه را کلیپ کنید"
},
"saveLinkOnly": {
"message": "فقط لینک را ذخیره کنید"
},
"pageNotAccessible": {
"message": "در اینجا نمی توان کلیپ کرد - این صفحه دسترسی برنامه های افزودنی را مسدود می کند."
},
"errLoginRequired": {
"message": "لطفاً ابتدا با این مرورگر وارد Memento شوید."
},
"errLoadNotebooks": {
"message": "نوت‌بوک‌ها بارگیری نشد. سعی کنید دوباره وصل شوید."
},
"notebooksLoaded": {
"message": "نوت بوک ها بارگیری شدند"
},
"connecting": {
"message": "در حال اتصال…"
},
"connectedToUrl": {
"message": "به $URL$ متصل شد",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "صفحه محدود - از طریق نوار ابزار Memento یا پانل جانبی کلیپ کنید."
},
"destinationNotebook": {
"message": "دفترچه یادداشت مقصد"
},
"activePage": {
"message": "صفحه فعال"
},
"previewBeforeSave": {
"message": "قبل از ذخیره بررسی کنید"
},
"noteTitleLabel": {
"message": "عنوان"
},
"excerptLabel": {
"message": "گزیده"
},
"saveToMomento": {
"message": "ذخیره در Memento"
},
"back": {
"message": "برگشت"
},
"analyzingSource": {
"message": "تجزیه و تحلیل منبع"
},
"statusAnalyzing": {
"message": "در حال تجزیه و تحلیل…"
},
"statusSaving": {
"message": "در حال ذخیره…"
},
"processingDetail": {
"message": "تولید برچسب ها، خلاصه معنایی، و جاسازی ها."
},
"noteSaved": {
"message": "یادداشت ذخیره شد"
},
"sentToNotebook": {
"message": "در $NOTEBOOK$ ذخیره شد",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "مشاهده در Memento"
},
"clipAnother": {
"message": "یک صفحه دیگر را کلیپ کنید"
},
"failure": {
"message": "تکمیل نشد"
},
"genericError": {
"message": "هنگام رسیدن به نمونه Memento شما مشکلی پیش آمد."
},
"retry": {
"message": "دوباره امتحان کنید"
},
"errNoSelection": {
"message": "ابتدا متن را انتخاب کنید، یا صفحه کامل را کلیپ کنید."
},
"errAnalyzeFailed": {
"message": "نمی توان این صفحه را تجزیه و تحلیل کرد."
},
"errSaveFailed": {
"message": "یادداشت شما ذخیره نشد."
},
"errNetwork": {
"message": "مشکل شبکه - اتصال و URL Memento خود را بررسی کنید."
},
"bannerPickText": {
"message": "متن را در صفحه برجسته کنید یا کل صفحه را برش دهید."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento · Web Clipper"
},
"extDescription": {
"message": "Enregistrez des pages web et du texte surligné dans vos carnets Memento — connecté à votre propre serveur Memento."
},
"extActionTitle": {
"message": "Clipper vers Memento"
},
"webClipper": {
"message": "Tondeuse Web"
},
"connected": {
"message": "Connecté"
},
"disconnected": {
"message": "Non connecté"
},
"instanceSettings": {
"message": "Adresse Memento"
},
"instanceUrlLabel": {
"message": "URL de votre instance Memento"
},
"presetProduction": {
"message": "Préréglage production · memento-note.com"
},
"applyReconnect": {
"message": "Appliquer et reconnecter"
},
"openMomento": {
"message": "Ouvrir Memento"
},
"settingsHint": {
"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.0"
},
"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"
},
"noNotebooks": {
"message": "Pas encore de carnets"
},
"readingTimeOne": {
"message": "≈ 1 minute de lecture"
},
"readingTimeOther": {
"message": "≈ $COUNT$ minutes de lecture",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Sélection détectée"
},
"ignore": {
"message": "ignorer"
},
"selectionHint": {
"message": "Astuce : surlignez du texte à lécran pour clipper une sélection précise de la page en tant que note."
},
"clipSelection": {
"message": "Clipper la sélection"
},
"clipPage": {
"message": "Clipper cette page"
},
"saveLinkOnly": {
"message": "Enregistrer le lien uniquement"
},
"pageNotAccessible": {
"message": "Impossible de découper ici : cette page bloque l'accès aux extensions."
},
"errLoginRequired": {
"message": "Veuillez d'abord vous connecter à Memento dans ce navigateur."
},
"errLoadNotebooks": {
"message": "Impossible de charger les blocs-notes. Essayez de vous reconnecter."
},
"notebooksLoaded": {
"message": "Carnets chargés"
},
"connecting": {
"message": "Connexion…"
},
"connectedToUrl": {
"message": "Connecté à $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Page restreinte : clip via la barre d'outils Memento ou le panneau latéral."
},
"destinationNotebook": {
"message": "Carnet de destination"
},
"activePage": {
"message": "Page active"
},
"previewBeforeSave": {
"message": "Vérifier avant d'enregistrer"
},
"noteTitleLabel": {
"message": "Titre"
},
"excerptLabel": {
"message": "Extrait"
},
"saveToMomento": {
"message": "Enregistrer dans Memento"
},
"back": {
"message": "Dos"
},
"analyzingSource": {
"message": "Analyse de la source"
},
"statusAnalyzing": {
"message": "Analyser…"
},
"statusSaving": {
"message": "Économie…"
},
"processingDetail": {
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
},
"noteSaved": {
"message": "Remarque enregistrée"
},
"sentToNotebook": {
"message": "Enregistré dans $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Voir dans Memento"
},
"clipAnother": {
"message": "Clipper une autre page"
},
"failure": {
"message": "Impossible de terminer"
},
"genericError": {
"message": "Une erreur est survenue lors de la communication avec votre instance Memento."
},
"retry": {
"message": "Réessayer"
},
"errNoSelection": {
"message": "Sélectionnez dabord le texte ou coupez la page entière."
},
"errAnalyzeFailed": {
"message": "Impossible d'analyser cette page."
},
"errSaveFailed": {
"message": "Impossible d'enregistrer votre note."
},
"errNetwork": {
"message": "Problème de réseau : vérifiez votre connexion et l'URL Memento."
},
"bannerPickText": {
"message": "Mettez en surbrillance le texte sur la page ou coupez la page entière."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "मोमेंटो वेब क्लिपर"
},
"extDescription": {
"message": "अपने मोमेंटो नोटबुक में वेब पेज और हाइलाइट किए गए टेक्स्ट को कैप्चर करें - यह आपके अपने मोमेंटो सर्वर से जुड़ता है।"
},
"extActionTitle": {
"message": "मोमेंटो पर क्लिप करें"
},
"webClipper": {
"message": "वेब क्लिपर"
},
"connected": {
"message": "जुड़े हुए"
},
"disconnected": {
"message": "जुड़े नहीं हैं"
},
"instanceSettings": {
"message": "मोमेंटो यूआरएल"
},
"instanceUrlLabel": {
"message": "आपका मोमेंटो इंस्टेंस यूआरएल"
},
"presetProduction": {
"message": "प्रोडक्शन प्रीसेट · memento-note.com"
},
"applyReconnect": {
"message": "आवेदन करें और पुनः कनेक्ट करें"
},
"openMomento": {
"message": "मोमेंटो खोलें"
},
"settingsHint": {
"message": "अपने मोमेंटो सर्वर का HTTPS (या LAN) URL चिपकाएँ। इस ब्राउज़र में कुकीज़ साइन-इन को संभालती हैं।"
},
"footerVersion": {
"message": "मोमेंटो वेब क्लिपर <<<संस्करण>>>"
},
"errPermissionDenied": {
"message": "मोमेंटो इस टैब तक नहीं पहुंच सकता. कीबोर्ड/साइट एक्सटेंशन अनुमतियां जांचें - या साइड पैनल खोलें।"
},
"notebookUnnamed": {
"message": "शीर्षक रहित नोटबुक"
},
"noNotebooks": {
"message": "अभी तक कोई नोटबुक नहीं"
},
"readingTimeOne": {
"message": "~1 मिनट पढ़ें"
},
"readingTimeOther": {
"message": "लगभग। $COUNT$ मिनट पढ़ा",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "चयन का पता चला"
},
"ignore": {
"message": "अनदेखा करना"
},
"selectionHint": {
"message": "युक्ति: किसी सटीक चयन को नोट के रूप में क्लिप करने के लिए पृष्ठ पर टेक्स्ट को हाइलाइट करें।"
},
"clipSelection": {
"message": "क्लिप चयन"
},
"clipPage": {
"message": "इस पृष्ठ को क्लिप करें"
},
"saveLinkOnly": {
"message": "केवल लिंक सहेजें"
},
"pageNotAccessible": {
"message": "यहां क्लिप नहीं किया जा सकता - यह पेज एक्सटेंशन एक्सेस को ब्लॉक करता है।"
},
"errLoginRequired": {
"message": "कृपया पहले इस ब्राउज़र में मोमेंटो में साइन इन करें।"
},
"errLoadNotebooks": {
"message": "नोटबुक लोड नहीं हो सकीं. पुनः कनेक्ट करने का प्रयास करें."
},
"notebooksLoaded": {
"message": "नोटबुक लोड किए गए"
},
"connecting": {
"message": "कनेक्ट हो रहा है..."
},
"connectedToUrl": {
"message": "$URL$ से कनेक्ट किया गया",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "प्रतिबंधित पृष्ठ - मोमेंटो टूलबार या साइड पैनल के माध्यम से क्लिप करें।"
},
"destinationNotebook": {
"message": "गंतव्य नोटबुक"
},
"activePage": {
"message": "सक्रिय पृष्ठ"
},
"previewBeforeSave": {
"message": "सहेजने से पहले समीक्षा करें"
},
"noteTitleLabel": {
"message": "शीर्षक"
},
"excerptLabel": {
"message": "अंश"
},
"saveToMomento": {
"message": "मोमेंटो में सहेजें"
},
"back": {
"message": "पीछे"
},
"analyzingSource": {
"message": "स्रोत का विश्लेषण"
},
"statusAnalyzing": {
"message": "विश्लेषण कर रहा हूँ..."
},
"statusSaving": {
"message": "सहेजा जा रहा है..."
},
"processingDetail": {
"message": "टैग, सिमेंटिक सारांश और एम्बेडिंग तैयार करना।"
},
"noteSaved": {
"message": "नोट सहेजा गया"
},
"sentToNotebook": {
"message": "$NOTEBOOK$ में सहेजा गया",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "मोमेंटो में देखें"
},
"clipAnother": {
"message": "दूसरे पेज को क्लिप करें"
},
"failure": {
"message": "पूरा नहीं हो सका"
},
"genericError": {
"message": "आपके मोमेंटो इंस्टेंस तक पहुँचने में कुछ गड़बड़ी हुई।"
},
"retry": {
"message": "पुन: प्रयास करें"
},
"errNoSelection": {
"message": "पहले टेक्स्ट चुनें, या पूरा पेज क्लिप करें।"
},
"errAnalyzeFailed": {
"message": "इस पृष्ठ का विश्लेषण नहीं किया जा सका."
},
"errSaveFailed": {
"message": "आपका नोट सहेजा नहीं जा सका."
},
"errNetwork": {
"message": "नेटवर्क समस्या - अपना कनेक्शन और मोमेंटो यूआरएल जांचें।"
},
"bannerPickText": {
"message": "पृष्ठ पर टेक्स्ट को हाइलाइट करें, या पूरे पृष्ठ को क्लिप करें।"
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Web Clipper"
},
"extDescription": {
"message": "Cattura pagine web e testo evidenziato nei tuoi taccuini Memento: si connette al tuo server Memento."
},
"extActionTitle": {
"message": "Clip su Memento"
},
"webClipper": {
"message": "Tagliatore di fotoricettore"
},
"connected": {
"message": "Collegato"
},
"disconnected": {
"message": "Non connesso"
},
"instanceSettings": {
"message": "URL del momento"
},
"instanceUrlLabel": {
"message": "L'URL dell'istanza di Memento"
},
"presetProduction": {
"message": "Preimpostazione di produzione · memento-note.com"
},
"applyReconnect": {
"message": "Applicare e riconnettersi"
},
"openMomento": {
"message": "Memento aperto"
},
"settingsHint": {
"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>>>"
},
"errPermissionDenied": {
"message": "Memento non può accedere a questa scheda. Controlla le autorizzazioni per tastiera/estensione del sito oppure apri il pannello laterale."
},
"notebookUnnamed": {
"message": "Taccuino senza titolo"
},
"noNotebooks": {
"message": "Nessun taccuino ancora"
},
"readingTimeOne": {
"message": "~1 minuto di lettura"
},
"readingTimeOther": {
"message": "ca. $COUNT$ min letto",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Selezione rilevata"
},
"ignore": {
"message": "ignorare"
},
"selectionHint": {
"message": "Suggerimento: evidenzia il testo sulla pagina per ritagliare una selezione precisa come nota."
},
"clipSelection": {
"message": "Selezione clip"
},
"clipPage": {
"message": "Ritaglia questa pagina"
},
"saveLinkOnly": {
"message": "Salva solo il collegamento"
},
"pageNotAccessible": {
"message": "Impossibile ritagliare qui: questa pagina blocca l'accesso all'estensione."
},
"errLoginRequired": {
"message": "Accedi prima a Memento in questo browser."
},
"errLoadNotebooks": {
"message": "Impossibile caricare i taccuini. Prova a riconnetterti."
},
"notebooksLoaded": {
"message": "Taccuini caricati"
},
"connecting": {
"message": "Connessione…"
},
"connectedToUrl": {
"message": "Connesso a $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Pagina limitata: ritaglia tramite la barra degli strumenti Memento o il pannello laterale."
},
"destinationNotebook": {
"message": "Taccuino di destinazione"
},
"activePage": {
"message": "Pagina attiva"
},
"previewBeforeSave": {
"message": "Rivedi prima di salvare"
},
"noteTitleLabel": {
"message": "Titolo"
},
"excerptLabel": {
"message": "Estratto"
},
"saveToMomento": {
"message": "Salva su Memento"
},
"back": {
"message": "Indietro"
},
"analyzingSource": {
"message": "Analisi della fonte"
},
"statusAnalyzing": {
"message": "Analizzando..."
},
"statusSaving": {
"message": "Risparmio…"
},
"processingDetail": {
"message": "Generazione di tag, riepilogo semantico e incorporamenti."
},
"noteSaved": {
"message": "Nota salvata"
},
"sentToNotebook": {
"message": "Salvato in $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Visualizza in Memento"
},
"clipAnother": {
"message": "Ritaglia un'altra pagina"
},
"failure": {
"message": "Impossibile completare"
},
"genericError": {
"message": "Qualcosa è andato storto nel raggiungere la tua istanza Memento."
},
"retry": {
"message": "Riprova"
},
"errNoSelection": {
"message": "Seleziona prima il testo o ritaglia l'intera pagina."
},
"errAnalyzeFailed": {
"message": "Impossibile analizzare questa pagina."
},
"errSaveFailed": {
"message": "Impossibile salvare la nota."
},
"errNetwork": {
"message": "Problema di rete: controlla la connessione e l'URL Memento."
},
"bannerPickText": {
"message": "Evidenzia il testo sulla pagina o ritaglia l'intera pagina."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "モーメントウェブクリッパー"
},
"extDescription": {
"message": "Web ページとハイライトされたテキストを Memento ノートブックにキャプチャします。独自の Memento サーバーに接続します。"
},
"extActionTitle": {
"message": "モーメントにクリップ"
},
"webClipper": {
"message": "ウェブクリッパー"
},
"connected": {
"message": "接続済み"
},
"disconnected": {
"message": "接続されていません"
},
"instanceSettings": {
"message": "モーメントのURL"
},
"instanceUrlLabel": {
"message": "Memento インスタンスの URL"
},
"presetProduction": {
"message": "プロダクションプリセット・memento-note.com"
},
"applyReconnect": {
"message": "適用して再接続する"
},
"openMomento": {
"message": "モーメントを開く"
},
"settingsHint": {
"message": "Memento サーバーの HTTPS (または LAN) URL を貼り付けます。このブラウザの Cookie がサインインを処理します。"
},
"footerVersion": {
"message": "Memento Web クリッパー <<<バージョン>>>"
},
"errPermissionDenied": {
"message": "Memento はこのタブにアクセスできません。キーボード/サイト拡張機能の権限を確認するか、サイド パネルを開きます。"
},
"notebookUnnamed": {
"message": "無題のノート"
},
"noNotebooks": {
"message": "まだノートはありません"
},
"readingTimeOne": {
"message": "約 1 分で読めます"
},
"readingTimeOther": {
"message": "約$COUNT$ 分読み取り",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "選択が検出されました"
},
"ignore": {
"message": "無視する"
},
"selectionHint": {
"message": "ヒント: ページ上のテキストをハイライト表示して、正確な選択範囲をメモとしてクリップします。"
},
"clipSelection": {
"message": "クリップの選択"
},
"clipPage": {
"message": "このページをクリップします"
},
"saveLinkOnly": {
"message": "リンクのみを保存"
},
"pageNotAccessible": {
"message": "ここではクリップできません — このページは拡張機能へのアクセスをブロックしています。"
},
"errLoginRequired": {
"message": "まずこのブラウザで Memento にサインインしてください。"
},
"errLoadNotebooks": {
"message": "ノートブックをロードできませんでした。再接続してみてください。"
},
"notebooksLoaded": {
"message": "ノートブックがロードされました"
},
"connecting": {
"message": "接続中…"
},
"connectedToUrl": {
"message": "$URL$ に接続しました",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "制限されたページ — Memento ツールバーまたはサイド パネルを介してクリップします。"
},
"destinationNotebook": {
"message": "宛先ノートブック"
},
"activePage": {
"message": "アクティブなページ"
},
"previewBeforeSave": {
"message": "保存する前に確認してください"
},
"noteTitleLabel": {
"message": "タイトル"
},
"excerptLabel": {
"message": "抜粋"
},
"saveToMomento": {
"message": "モーメントに保存"
},
"back": {
"message": "戻る"
},
"analyzingSource": {
"message": "ソースを分析中"
},
"statusAnalyzing": {
"message": "分析中…"
},
"statusSaving": {
"message": "保存中…"
},
"processingDetail": {
"message": "タグ、意味の概要、および埋め込みを生成します。"
},
"noteSaved": {
"message": "メモが保存されました"
},
"sentToNotebook": {
"message": "$NOTEBOOK$ に保存されました",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "モメントで見る"
},
"clipAnother": {
"message": "別のページをクリップする"
},
"failure": {
"message": "完了できませんでした"
},
"genericError": {
"message": "Memento インスタンスに到達する際に問題が発生しました。"
},
"retry": {
"message": "リトライ"
},
"errNoSelection": {
"message": "最初にテキストを選択するか、ページ全体をクリップします。"
},
"errAnalyzeFailed": {
"message": "このページを分析できませんでした。"
},
"errSaveFailed": {
"message": "メモを保存できませんでした。"
},
"errNetwork": {
"message": "ネットワークの問題 — 接続と Memento URL を確認してください。"
},
"bannerPickText": {
"message": "ページ上のテキストを強調表示するか、ページ全体をクリップします。"
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "모멘토 웹 클리퍼"
},
"extDescription": {
"message": "웹 페이지와 강조 표시된 텍스트를 Memento 노트북에 캡처하여 자체 Memento 서버에 연결합니다."
},
"extActionTitle": {
"message": "순간에 클립"
},
"webClipper": {
"message": "웹 클리퍼"
},
"connected": {
"message": "연결됨"
},
"disconnected": {
"message": "연결되지 않음"
},
"instanceSettings": {
"message": "모멘토 URL"
},
"instanceUrlLabel": {
"message": "귀하의 Memento 인스턴스 URL"
},
"presetProduction": {
"message": "프로덕션 프리셋 · memento-note.com"
},
"applyReconnect": {
"message": "적용하고 다시 연결하세요"
},
"openMomento": {
"message": "모멘토 열기"
},
"settingsHint": {
"message": "Memento 서버의 HTTPS(또는 LAN) URL을 붙여넣습니다. 이 브라우저의 쿠키는 로그인을 처리합니다."
},
"footerVersion": {
"message": "Memento Web Clipper <<<버전>>>"
},
"errPermissionDenied": {
"message": "Memento는 이 탭에 접근할 수 없습니다. 키보드/사이트 확장 권한을 확인하거나 측면 패널을 엽니다."
},
"notebookUnnamed": {
"message": "제목 없는 노트"
},
"noNotebooks": {
"message": "아직 노트가 없습니다."
},
"readingTimeOne": {
"message": "~1분 읽기"
},
"readingTimeOther": {
"message": "대략. $COUNT$분 읽음",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "선택 항목이 감지되었습니다."
},
"ignore": {
"message": "무시하다"
},
"selectionHint": {
"message": "팁: 페이지의 텍스트를 강조 표시하여 정확한 선택 항목을 메모로 자릅니다."
},
"clipSelection": {
"message": "클립 선택"
},
"clipPage": {
"message": "이 페이지 클립"
},
"saveLinkOnly": {
"message": "링크만 저장"
},
"pageNotAccessible": {
"message": "여기서 클립할 수 없습니다. 이 페이지는 확장 프로그램 액세스를 차단합니다."
},
"errLoginRequired": {
"message": "먼저 이 브라우저에서 Memento에 로그인하세요."
},
"errLoadNotebooks": {
"message": "노트북을 로드할 수 없습니다. 다시 연결해 보세요."
},
"notebooksLoaded": {
"message": "노트북이 로드되었습니다."
},
"connecting": {
"message": "연결 중…"
},
"connectedToUrl": {
"message": "$URL$에 연결됨",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "제한된 페이지 — Memento 도구 모음 또는 측면 패널을 통해 클립합니다."
},
"destinationNotebook": {
"message": "대상 노트북"
},
"activePage": {
"message": "활성 페이지"
},
"previewBeforeSave": {
"message": "저장하기 전에 검토하세요"
},
"noteTitleLabel": {
"message": "제목"
},
"excerptLabel": {
"message": "발췌"
},
"saveToMomento": {
"message": "모멘토에 저장"
},
"back": {
"message": "뒤쪽에"
},
"analyzingSource": {
"message": "소스 분석 중"
},
"statusAnalyzing": {
"message": "분석 중…"
},
"statusSaving": {
"message": "절약…"
},
"processingDetail": {
"message": "태그, 의미 요약 및 임베딩을 생성합니다."
},
"noteSaved": {
"message": "메모가 저장되었습니다."
},
"sentToNotebook": {
"message": "$NOTEBOOK$에 저장됨",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Memento에서 보기"
},
"clipAnother": {
"message": "다른 페이지 자르기"
},
"failure": {
"message": "완료할 수 없습니다."
},
"genericError": {
"message": "Memento 인스턴스에 연결하는 데 문제가 발생했습니다."
},
"retry": {
"message": "다시 해 보다"
},
"errNoSelection": {
"message": "먼저 텍스트를 선택하거나 전체 페이지를 자릅니다."
},
"errAnalyzeFailed": {
"message": "이 페이지를 분석할 수 없습니다."
},
"errSaveFailed": {
"message": "메모를 저장할 수 없습니다."
},
"errNetwork": {
"message": "네트워크 문제 - 연결 및 Memento URL을 확인하세요."
},
"bannerPickText": {
"message": "페이지의 텍스트를 강조 표시하거나 전체 페이지를 자릅니다."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Webclipper"
},
"extDescription": {
"message": "Leg webpagina's en gemarkeerde tekst vast in uw Memento-notebooks - maakt verbinding met uw eigen Memento-server."
},
"extActionTitle": {
"message": "Clip naar Memento"
},
"webClipper": {
"message": "Webclipper"
},
"connected": {
"message": "Aangesloten"
},
"disconnected": {
"message": "Niet verbonden"
},
"instanceSettings": {
"message": "Memento-URL"
},
"instanceUrlLabel": {
"message": "Uw Memento-instantie-URL"
},
"presetProduction": {
"message": "Productievoorinstelling · memento-note.com"
},
"applyReconnect": {
"message": "Toepassen en opnieuw verbinden"
},
"openMomento": {
"message": "Memento openen"
},
"settingsHint": {
"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>>>"
},
"errPermissionDenied": {
"message": "Memento heeft geen toegang tot dit tabblad. Controleer de rechten voor toetsenbord-/site-extensies — of open het zijpaneel."
},
"notebookUnnamed": {
"message": "Naamloos notitieboekje"
},
"noNotebooks": {
"message": "Nog geen notitieboekjes"
},
"readingTimeOne": {
"message": "~1 minuut lezen"
},
"readingTimeOther": {
"message": "Ongeveer. $COUNT$ min gelezen",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Selectie gedetecteerd"
},
"ignore": {
"message": "negeren"
},
"selectionHint": {
"message": "Tip: markeer tekst op de pagina om een precieze selectie als notitie te knippen."
},
"clipSelection": {
"message": "Clipselectie"
},
"clipPage": {
"message": "Knip deze pagina uit"
},
"saveLinkOnly": {
"message": "Alleen link opslaan"
},
"pageNotAccessible": {
"message": "Kan hier niet knippen: deze pagina blokkeert de toegang tot extensies."
},
"errLoginRequired": {
"message": "Meld u eerst aan bij Memento in deze browser."
},
"errLoadNotebooks": {
"message": "Kan notitieboekjes niet laden. Probeer opnieuw verbinding te maken."
},
"notebooksLoaded": {
"message": "Notitieboekjes geladen"
},
"connecting": {
"message": "Verbinden…"
},
"connectedToUrl": {
"message": "Verbonden met $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Beperkte pagina — clip via de Memento-werkbalk of het zijpaneel."
},
"destinationNotebook": {
"message": "Bestemmingsnotitieboekje"
},
"activePage": {
"message": "Actieve pagina"
},
"previewBeforeSave": {
"message": "Controleer voordat u opslaat"
},
"noteTitleLabel": {
"message": "Titel"
},
"excerptLabel": {
"message": "Uittreksel"
},
"saveToMomento": {
"message": "Opslaan in Memento"
},
"back": {
"message": "Rug"
},
"analyzingSource": {
"message": "Bron analyseren"
},
"statusAnalyzing": {
"message": "Analyseren…"
},
"statusSaving": {
"message": "Besparing…"
},
"processingDetail": {
"message": "Tags, een semantische samenvatting en insluitingen genereren."
},
"noteSaved": {
"message": "Notitie opgeslagen"
},
"sentToNotebook": {
"message": "Opgeslagen in $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Bekijk in Memento"
},
"clipAnother": {
"message": "Knip nog een pagina uit"
},
"failure": {
"message": "Kon niet voltooien"
},
"genericError": {
"message": "Er is iets misgegaan bij het bereiken van uw Memento-instantie."
},
"retry": {
"message": "Opnieuw proberen"
},
"errNoSelection": {
"message": "Selecteer eerst tekst of knip de volledige pagina uit."
},
"errAnalyzeFailed": {
"message": "Kan deze pagina niet analyseren."
},
"errSaveFailed": {
"message": "Kan uw notitie niet opslaan."
},
"errNetwork": {
"message": "Netwerkprobleem: controleer uw verbinding en Memento-URL."
},
"bannerPickText": {
"message": "Markeer tekst op de pagina of knip de hele pagina uit."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Narzędzie do strzyżenia sieci Memento"
},
"extDescription": {
"message": "Przechwytuj strony internetowe i zaznaczony tekst do swoich notatników Memento — łączy się z Twoim własnym serwerem Memento."
},
"extActionTitle": {
"message": "Klip do Memento"
},
"webClipper": {
"message": "Obcinacz sieci"
},
"connected": {
"message": "Połączony"
},
"disconnected": {
"message": "Nie podłączony"
},
"instanceSettings": {
"message": "Adres URL chwili"
},
"instanceUrlLabel": {
"message": "Adres URL Twojej instancji Memento"
},
"presetProduction": {
"message": "Wstępne ustawienia produkcyjne · memento-note.com"
},
"applyReconnect": {
"message": "Zastosuj i połącz ponownie"
},
"openMomento": {
"message": "Otwórz Memento"
},
"settingsHint": {
"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>>>"
},
"errPermissionDenied": {
"message": "Memento nie ma dostępu do tej karty. Sprawdź uprawnienia rozszerzenia klawiatury/witryny — lub otwórz Panel boczny."
},
"notebookUnnamed": {
"message": "Notatnik bez tytułu"
},
"noNotebooks": {
"message": "Nie ma jeszcze żadnych notatników"
},
"readingTimeOne": {
"message": "~1 minuta czytania"
},
"readingTimeOther": {
"message": "Około. $COUNT$ min odczytu",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Wykryto wybór"
},
"ignore": {
"message": "ignorować"
},
"selectionHint": {
"message": "Wskazówka: zaznacz tekst na stronie, aby wyciąć dokładne zaznaczenie jako notatkę."
},
"clipSelection": {
"message": "Wybór klipu"
},
"clipPage": {
"message": "Przytnij tę stronę"
},
"saveLinkOnly": {
"message": "Zapisz tylko link"
},
"pageNotAccessible": {
"message": "Nie można tutaj przyciąć — ta strona blokuje dostęp do rozszerzenia."
},
"errLoginRequired": {
"message": "Najpierw zaloguj się do Memento w tej przeglądarce."
},
"errLoadNotebooks": {
"message": "Nie udało się załadować notatników. Spróbuj połączyć się ponownie."
},
"notebooksLoaded": {
"message": "Notatniki załadowane"
},
"connecting": {
"message": "Złączony…"
},
"connectedToUrl": {
"message": "Połączono z $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Strona z ograniczeniami — klip za pomocą paska narzędzi Memento lub panelu bocznego."
},
"destinationNotebook": {
"message": "Notatnik docelowy"
},
"activePage": {
"message": "Aktywna strona"
},
"previewBeforeSave": {
"message": "Przejrzyj przed zapisaniem"
},
"noteTitleLabel": {
"message": "Tytuł"
},
"excerptLabel": {
"message": "Fragment"
},
"saveToMomento": {
"message": "Zapisz w Memento"
},
"back": {
"message": "Z powrotem"
},
"analyzingSource": {
"message": "Analizowanie źródła"
},
"statusAnalyzing": {
"message": "Analizuję…"
},
"statusSaving": {
"message": "Oszczędność…"
},
"processingDetail": {
"message": "Generowanie tagów, podsumowań semantycznych i osadzania."
},
"noteSaved": {
"message": "Uwaga zapisana"
},
"sentToNotebook": {
"message": "Zapisano w $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Zobacz w Memento"
},
"clipAnother": {
"message": "Wytnij kolejną stronę"
},
"failure": {
"message": "Nie udało się ukończyć"
},
"genericError": {
"message": "Coś poszło nie tak podczas docierania do Twojej instancji Memento."
},
"retry": {
"message": "Spróbować ponownie"
},
"errNoSelection": {
"message": "Najpierw zaznacz tekst lub wytnij całą stronę."
},
"errAnalyzeFailed": {
"message": "Nie można przeanalizować tej strony."
},
"errSaveFailed": {
"message": "Nie można zapisać notatki."
},
"errNetwork": {
"message": "Problem z siecią — sprawdź swoje połączenie i adres URL Memento."
},
"bannerPickText": {
"message": "Zaznacz tekst na stronie lub przytnij całą stronę."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento Web Clipper"
},
"extDescription": {
"message": "Capture páginas da web e texto destacado em seus blocos de anotações Memento conecte-se ao seu próprio servidor Memento."
},
"extActionTitle": {
"message": "Clipe para Memento"
},
"webClipper": {
"message": "Clipper da Web"
},
"connected": {
"message": "Conectado"
},
"disconnected": {
"message": "Não conectado"
},
"instanceSettings": {
"message": "URL do momento"
},
"instanceUrlLabel": {
"message": "URL da sua instância do Memento"
},
"presetProduction": {
"message": "Predefinição de produção · memento-note.com"
},
"applyReconnect": {
"message": "Aplicar e reconectar"
},
"openMomento": {
"message": "Memento aberto"
},
"settingsHint": {
"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>>>"
},
"errPermissionDenied": {
"message": "Memento não pode acessar esta guia. Verifique as permissões de extensão de teclado/site ou abra o painel lateral."
},
"notebookUnnamed": {
"message": "Caderno sem título"
},
"noNotebooks": {
"message": "Ainda não há cadernos"
},
"readingTimeOne": {
"message": "~1 minuto de leitura"
},
"readingTimeOther": {
"message": "Aprox. $COUNT$ minutos de leitura",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Seleção detectada"
},
"ignore": {
"message": "ignorar"
},
"selectionHint": {
"message": "Dica: destaque o texto na página para recortar uma seleção precisa como uma nota."
},
"clipSelection": {
"message": "Seleção de clipe"
},
"clipPage": {
"message": "Recorte esta página"
},
"saveLinkOnly": {
"message": "Salvar apenas link"
},
"pageNotAccessible": {
"message": "Não é possível recortar aqui esta página bloqueia o acesso à extensão."
},
"errLoginRequired": {
"message": "Faça login no Memento neste navegador primeiro."
},
"errLoadNotebooks": {
"message": "Não foi possível carregar os notebooks. Tente reconectar."
},
"notebooksLoaded": {
"message": "Cadernos carregados"
},
"connecting": {
"message": "Conectando…"
},
"connectedToUrl": {
"message": "Conectado a $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Página restrita recorte por meio da barra de ferramentas do Memento ou do painel lateral."
},
"destinationNotebook": {
"message": "Caderno de destino"
},
"activePage": {
"message": "Página ativa"
},
"previewBeforeSave": {
"message": "Revise antes de salvar"
},
"noteTitleLabel": {
"message": "Título"
},
"excerptLabel": {
"message": "Trecho"
},
"saveToMomento": {
"message": "Salvar no Memento"
},
"back": {
"message": "Voltar"
},
"analyzingSource": {
"message": "Analisando fonte"
},
"statusAnalyzing": {
"message": "Analisando…"
},
"statusSaving": {
"message": "Salvando…"
},
"processingDetail": {
"message": "Geração de tags, resumo semântico e incorporações."
},
"noteSaved": {
"message": "Nota salva"
},
"sentToNotebook": {
"message": "Salvo em $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Ver em Memento"
},
"clipAnother": {
"message": "Recortar outra página"
},
"failure": {
"message": "Não foi possível concluir"
},
"genericError": {
"message": "Algo deu errado ao chegar à sua instância do Memento."
},
"retry": {
"message": "Tentar novamente"
},
"errNoSelection": {
"message": "Selecione o texto primeiro ou recorte a página inteira."
},
"errAnalyzeFailed": {
"message": "Não foi possível analisar esta página."
},
"errSaveFailed": {
"message": "Não foi possível salvar sua nota."
},
"errNetwork": {
"message": "Problema de rede verifique sua conexão e URL do Memento."
},
"bannerPickText": {
"message": "Destaque o texto na página ou recorte a página inteira."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Веб-клипер Memento"
},
"extDescription": {
"message": "Сохраняйте веб-страницы и выделенный текст в свои блокноты Memento — подключайтесь к вашему собственному серверу Memento."
},
"extActionTitle": {
"message": "Клип на Memento"
},
"webClipper": {
"message": "Веб-клипер"
},
"connected": {
"message": "Подключено"
},
"disconnected": {
"message": "Не подключено"
},
"instanceSettings": {
"message": "URL-адрес момента"
},
"instanceUrlLabel": {
"message": "URL-адрес вашего экземпляра Memento"
},
"presetProduction": {
"message": "Настройки производства · memento-note.com"
},
"applyReconnect": {
"message": "Подать заявку и повторно подключиться"
},
"openMomento": {
"message": "Открыть Моменто"
},
"settingsHint": {
"message": "Вставьте URL-адрес HTTPS (или LAN) вашего сервера Memento. Файлы cookie в этом браузере обрабатывают вход в систему."
},
"footerVersion": {
"message": "Memento Web Clipper <<<ВЕРСИЯ>>>"
},
"errPermissionDenied": {
"message": "Memento не имеет доступа к этой вкладке. Проверьте разрешения для расширения клавиатуры/сайта или откройте боковую панель."
},
"notebookUnnamed": {
"message": "Блокнот без названия"
},
"noNotebooks": {
"message": "Блокнотов пока нет"
},
"readingTimeOne": {
"message": "~1 минута чтения"
},
"readingTimeOther": {
"message": "Прибл. $COUNT$ мин чтения",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "Выбор обнаружен"
},
"ignore": {
"message": "игнорировать"
},
"selectionHint": {
"message": "Совет: выделите текст на странице, чтобы выделить его в виде заметки."
},
"clipSelection": {
"message": "Выбор клипа"
},
"clipPage": {
"message": "Вырезать эту страницу"
},
"saveLinkOnly": {
"message": "Сохранить только ссылку"
},
"pageNotAccessible": {
"message": "Невозможно обрезать здесь — эта страница блокирует доступ к расширению."
},
"errLoginRequired": {
"message": "Сначала войдите в Memento в этом браузере."
},
"errLoadNotebooks": {
"message": "Не удалось загрузить блокноты. Попробуйте переподключиться."
},
"notebooksLoaded": {
"message": "Ноутбуки загружены"
},
"connecting": {
"message": "Подключение…"
},
"connectedToUrl": {
"message": "Подключено к $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "Страница с ограниченным доступом — вырезайте с помощью панели инструментов Memento или боковой панели."
},
"destinationNotebook": {
"message": "Блокнот назначения"
},
"activePage": {
"message": "Активная страница"
},
"previewBeforeSave": {
"message": "Проверьте перед сохранением"
},
"noteTitleLabel": {
"message": "Заголовок"
},
"excerptLabel": {
"message": "Отрывок"
},
"saveToMomento": {
"message": "Сохранить в Моменто"
},
"back": {
"message": "Назад"
},
"analyzingSource": {
"message": "Анализ источника"
},
"statusAnalyzing": {
"message": "Анализ…"
},
"statusSaving": {
"message": "Сохранение…"
},
"processingDetail": {
"message": "Генерация тегов, семантического резюме и вложений."
},
"noteSaved": {
"message": "Заметка сохранена."
},
"sentToNotebook": {
"message": "Сохранено в $NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "Посмотреть в Моменто"
},
"clipAnother": {
"message": "Вырезать другую страницу"
},
"failure": {
"message": "Не удалось завершить"
},
"genericError": {
"message": "Что-то пошло не так при получении вашего экземпляра Memento."
},
"retry": {
"message": "Повторить попытку"
},
"errNoSelection": {
"message": "Сначала выделите текст или вырежьте всю страницу."
},
"errAnalyzeFailed": {
"message": "Не удалось проанализировать эту страницу."
},
"errSaveFailed": {
"message": "Не удалось сохранить заметку."
},
"errNetwork": {
"message": "Проблема с сетью. Проверьте подключение и URL-адрес Memento."
},
"bannerPickText": {
"message": "Выделите текст на странице или вырежьте всю страницу."
}
}

View File

@@ -0,0 +1,182 @@
{
"extName": {
"message": "Memento 网页剪辑器"
},
"extDescription": {
"message": "将网页和突出显示的文本捕获到您的 Memento 笔记本中 — 连接到您自己的 Memento 服务器。"
},
"extActionTitle": {
"message": "剪辑到时刻"
},
"webClipper": {
"message": "网页剪辑器"
},
"connected": {
"message": "已连接"
},
"disconnected": {
"message": "未连接"
},
"instanceSettings": {
"message": "时刻网址"
},
"instanceUrlLabel": {
"message": "您的 Memento 实例 URL"
},
"presetProduction": {
"message": "制作预设 · memento-note.com"
},
"applyReconnect": {
"message": "应用并重新连接"
},
"openMomento": {
"message": "打开时刻"
},
"settingsHint": {
"message": "粘贴 Memento 服务器的 HTTPS或 LANURL。此浏览器中的 Cookie 处理登录。"
},
"footerVersion": {
"message": "Memento Web Clipper <<<版本>>>"
},
"errPermissionDenied": {
"message": "Memento 无法访问此选项卡。检查键盘/站点扩展权限 - 或打开侧面板。"
},
"notebookUnnamed": {
"message": "无标题笔记本"
},
"noNotebooks": {
"message": "还没有笔记本"
},
"readingTimeOne": {
"message": "阅读约 1 分钟"
},
"readingTimeOther": {
"message": "大约。 $COUNT$ 最少阅读次数",
"placeholders": {
"COUNT": {
"content": "$1",
"example": "5"
}
}
},
"selectionDetected": {
"message": "检测到选择"
},
"ignore": {
"message": "忽略"
},
"selectionHint": {
"message": "提示:突出显示页面上的文本以将精确的选择剪辑为注释。"
},
"clipSelection": {
"message": "剪辑选择"
},
"clipPage": {
"message": "剪辑此页"
},
"saveLinkOnly": {
"message": "仅保存链接"
},
"pageNotAccessible": {
"message": "此处无法剪辑 — 此页面阻止扩展程序访问。"
},
"errLoginRequired": {
"message": "请先在此浏览器中登录 Memento。"
},
"errLoadNotebooks": {
"message": "无法加载笔记本。尝试重新连接。"
},
"notebooksLoaded": {
"message": "笔记本已加载"
},
"connecting": {
"message": "正在连接…"
},
"connectedToUrl": {
"message": "连接到 $URL$",
"placeholders": {
"URL": {
"content": "$1",
"example": "https://memento-note.com"
}
}
},
"restrictedPage": {
"message": "受限页面 — 通过 Memento 工具栏或侧面板进行剪辑。"
},
"destinationNotebook": {
"message": "目的地笔记本"
},
"activePage": {
"message": "活动页面"
},
"previewBeforeSave": {
"message": "保存前查看"
},
"noteTitleLabel": {
"message": "标题"
},
"excerptLabel": {
"message": "摘抄"
},
"saveToMomento": {
"message": "保存到时刻"
},
"back": {
"message": "后退"
},
"analyzingSource": {
"message": "分析来源"
},
"statusAnalyzing": {
"message": "正在分析……"
},
"statusSaving": {
"message": "保存…"
},
"processingDetail": {
"message": "生成标签、语义摘要和嵌入。"
},
"noteSaved": {
"message": "注释已保存"
},
"sentToNotebook": {
"message": "保存至$NOTEBOOK$",
"placeholders": {
"NOTEBOOK": {
"content": "$1",
"example": "Read later"
}
}
},
"viewInMomento": {
"message": "在 Memento 中查看"
},
"clipAnother": {
"message": "剪辑另一页"
},
"failure": {
"message": "无法完成"
},
"genericError": {
"message": "您的 Memento 实例出现问题。"
},
"retry": {
"message": "重试"
},
"errNoSelection": {
"message": "首先选择文本,或剪辑整个页面。"
},
"errAnalyzeFailed": {
"message": "无法分析此页面。"
},
"errSaveFailed": {
"message": "无法保存您的笔记。"
},
"errNetwork": {
"message": "网络问题 — 检查您的连接和 Memento URL。"
},
"bannerPickText": {
"message": "突出显示页面上的文本,或剪辑整个页面。"
}
}

View File

@@ -0,0 +1,23 @@
/** Service worker — ouvre le panneau latéral (Chrome Side Panel ou Firefox sidebar) au clic sur l'icône. */
const extApi = (typeof browser !== 'undefined' && browser.sidebarAction) ? browser : chrome
if (extApi.sidePanel?.setPanelBehavior) {
// Chrome MV3 : Side Panel API
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
} else if (extApi.sidebarAction?.open) {
// Firefox MV3 : sidebarAction
extApi.action?.onClicked?.addListener?.(() => {
extApi.sidebarAction.open().catch(() => {})
})
}
extApi.runtime?.onInstalled?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})
extApi.runtime?.onStartup?.addListener?.(() => {
if (extApi.sidePanel?.setPanelBehavior) {
extApi.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}
})

View File

@@ -0,0 +1,211 @@
/**
* Content script Memento — sélection live, surlignage, communication avec le side panel.
* Injecté automatiquement sur http(s) ; ré-injecté à la demande si longlet était déjà ouvert.
*/
;(function initMementoClipperContent() {
if (globalThis.__mementoClipperContent) return
globalThis.__mementoClipperContent = true
const HIGHLIGHT_ID = 'memento-clipper-highlight-root'
const BANNER_ID = 'memento-clipper-banner-root'
const STYLE_ID = 'memento-clipper-styles'
let pickMode = false
let debounceTimer = null
function getSelectionText() {
return window.getSelection()?.toString().trim() || ''
}
function getPageMeta() {
const dir =
document.documentElement.getAttribute('dir') ||
document.body?.getAttribute('dir') ||
''
const lang = (
document.documentElement.getAttribute('lang') ||
document.body?.getAttribute('lang') ||
''
).split('-')[0]
return {
text: getSelectionText(),
dir,
lang,
url: location.href,
title: document.title,
}
}
function broadcastSelection() {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() }
try {
chrome.runtime.sendMessage(payload).catch(() => {})
} catch {
/* ignore */
}
if (pickMode) paintHighlight()
}, 80)
}
function removeHighlight() {
document.getElementById(HIGHLIGHT_ID)?.remove()
}
function paintHighlight() {
removeHighlight()
const sel = window.getSelection()
if (!sel || sel.isCollapsed || !sel.rangeCount) return
let range
try {
range = sel.getRangeAt(0)
} catch {
return
}
const host = document.createElement('div')
host.id = HIGHLIGHT_ID
host.setAttribute('aria-hidden', 'true')
host.style.cssText =
'position:fixed;inset:0;pointer-events:none;z-index:2147483644;overflow:hidden;'
for (const rect of range.getClientRects()) {
if (rect.width < 2 || rect.height < 2) continue
const box = document.createElement('div')
box.style.cssText = [
'position:fixed',
`left:${rect.left - 2}px`,
`top:${rect.top - 1}px`,
`width:${rect.width + 4}px`,
`height:${rect.height + 2}px`,
'background:rgba(164,113,72,0.28)',
'border-radius:3px',
'box-shadow:0 0 0 1px rgba(164,113,72,0.35)',
'transition:opacity 0.15s ease',
].join(';')
host.appendChild(box)
}
if (host.childNodes.length) document.documentElement.appendChild(host)
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return
const style = document.createElement('style')
style.id = STYLE_ID
style.textContent = `
html.memento-clipper-pick ::selection {
background: rgba(164, 113, 72, 0.45) !important;
color: inherit !important;
}
html.memento-clipper-pick {
scroll-behavior: auto;
}
`
document.documentElement.appendChild(style)
}
function removeBanner() {
document.getElementById(BANNER_ID)?.remove()
}
function ensureBanner() {
if (document.getElementById(BANNER_ID)) return
const bannerText =
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
'Highlight the text to clip'
const host = document.createElement('div')
host.id = BANNER_ID
host.style.cssText =
'all:initial;position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:none;font-family:Inter,system-ui,sans-serif;'
const shadow = host.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.pill {
display: flex; align-items: center; gap: 10px;
padding: 10px 18px; border-radius: 999px;
background: #1c1c1c; color: #faf9f5;
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
font-size: 12px; font-weight: 600;
letter-spacing: 0.02em;
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
}
.logo {
width: 22px; height: 22px; border-radius: 7px;
background: #faf9f5; color: #1c1c1c;
display: flex; align-items: center; justify-content: center;
font-family: Georgia, serif; font-weight: 900; font-size: 12px;
}
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: #a47148; animation: pulse 1.2s ease infinite;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
</style>
<div class="pill">
<span class="logo">M</span>
<span class="dot"></span>
<span>${bannerText.replace(/</g, '&lt;')}</span>
</div>
`
document.documentElement.appendChild(host)
}
function setPickMode(enabled) {
pickMode = !!enabled
ensureStyles()
if (pickMode) {
document.documentElement.classList.add('memento-clipper-pick')
ensureBanner()
paintHighlight()
} else {
document.documentElement.classList.remove('memento-clipper-pick')
removeBanner()
removeHighlight()
}
}
function onScrollOrResize() {
if (pickMode) paintHighlight()
}
document.addEventListener('selectionchange', broadcastSelection)
document.addEventListener('mouseup', broadcastSelection)
document.addEventListener('keyup', broadcastSelection)
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
window.addEventListener('resize', onScrollOrResize, { passive: true })
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === 'PING') {
sendResponse({ ok: true })
return true
}
if (message?.type === 'GET_CONTEXT') {
sendResponse({
html: document.documentElement.outerHTML,
...getPageMeta(),
})
return true
}
if (message?.type === 'SET_PICK_MODE') {
setPickMode(!!message.enabled)
sendResponse({ ok: true, pickMode })
return true
}
return false
})
broadcastSelection()
})()

View File

@@ -0,0 +1,47 @@
/** Helpers i18n — langue UI Chrome (chrome.i18n.getUILanguage). */
function t(key, ...subs) {
const msg = chrome.i18n.getMessage(key, subs)
return msg || key
}
function uiLocaleTag() {
return (chrome.i18n.getUILanguage() || 'en').replace('_', '-').split('-')[0]
}
function isUiRtl() {
return ['ar', 'fa', 'he', 'ur'].includes(uiLocaleTag())
}
function applyDocumentLocale() {
const lang = uiLocaleTag()
document.documentElement.lang = lang
document.documentElement.dir = isUiRtl() ? 'rtl' : 'ltr'
document.body?.classList.toggle('ui-rtl', isUiRtl())
}
function applyShellI18n() {
document.title = t('extName')
const brandSub = document.querySelector('.brand-sub')
if (brandSub) brandSub.textContent = t('webClipper')
if (typeof els !== 'undefined' && els.connLabel) {
els.connLabel.textContent = t('connected')
}
if (typeof els !== 'undefined' && els.settingsBtn) {
els.settingsBtn.title = t('instanceSettings')
els.settingsBtn.setAttribute('aria-label', t('instanceSettings'))
}
const urlLabel = document.getElementById('instanceUrlLabel') || document.querySelector('#settingsPanel .field > span')
if (urlLabel) urlLabel.textContent = t('instanceUrlLabel')
const presetProd = document.querySelector('[data-url="https://memento-note.com"]')
if (presetProd) presetProd.textContent = t('presetProduction')
if (typeof els !== 'undefined' && els.applyInstanceBtn) {
els.applyInstanceBtn.textContent = t('applyReconnect')
}
if (typeof els !== 'undefined' && els.openLoginBtn) {
els.openLoginBtn.textContent = `${t('openMomento')}`
}
const hint = document.querySelector('.settings-hint')
if (hint) hint.textContent = t('settingsHint')
const footer = document.querySelector('.footer-meta')
if (footer) footer.textContent = t('footerVersion')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#1C1C1C" rx="40"/>
<g transform="translate(96,96)">
<rect x="-34" y="-34" width="68" height="68" rx="5" fill="white"/>
<path d="M34,34 L34,11 L11,34 Z" fill="#1C1C1C" opacity="0.28"/>
<line x1="-23" y1="-17" x2="23" y2="-17" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
<line x1="-23" y1="-4" x2="23" y2="-4" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
<line x1="-23" y1="9" x2="11" y2="9" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#1C1C1C" rx="100"/>
<g transform="translate(256,256)">
<rect x="-90" y="-90" width="180" height="180" rx="12" fill="white"/>
<path d="M90,90 L90,30 L30,90 Z" fill="#1C1C1C" opacity="0.28"/>
<line x1="-60" y1="-45" x2="60" y2="-45" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
<line x1="-60" y1="-10" x2="60" y2="-10" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
<line x1="-60" y1="25" x2="30" y2="25" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -0,0 +1,65 @@
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "0.4.0",
"description": "__MSG_extDescription__",
"default_locale": "en",
"permissions": [
"activeTab",
"scripting",
"storage",
"sidePanel",
"tabs"
],
"host_permissions": [
"https://memento-note.com/*",
"https://www.memento-note.com/*"
],
"optional_host_permissions": [
"https://*/*"
],
"background": {
"service_worker": "background.js"
},
"sidebar_action": {
"default_title": "__MSG_extActionTitle__",
"default_panel": "sidepanel.html",
"default_icon": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
},
"browser_specific_settings": {
"gecko": {
"id": "memento-clipper@memento-note.com",
"strict_min_version": "115.0"
}
},
"content_scripts": [
{
"matches": [
"https://memento-note.com/*",
"https://www.memento-note.com/*"
],
"js": [
"content.js"
],
"run_at": "document_idle",
"all_frames": false
}
],
"action": {
"default_title": "__MSG_extActionTitle__",
"default_icon": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
},
"icons": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
}

View File

@@ -0,0 +1,507 @@
:root {
--ink: #1c1c1c;
--paper: #faf9f5;
--card: #ffffff;
--muted: #6b7280;
--border: #e8e4dc;
--accent: #a47148;
--accent-soft: rgba(164, 113, 72, 0.12);
--accent-glow: rgba(164, 113, 72, 0.35);
--success: #10b981;
--danger: #ef4444;
--shadow: 0 18px 40px rgba(28, 28, 28, 0.08);
--radius: 14px;
--radius-sm: 10px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 13px;
color: var(--ink);
background: var(--paper);
}
.shell {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--paper);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 18px;
background: linear-gradient(180deg, #fff 0%, #fcfcfa 100%);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-logo {
width: 34px; height: 34px; border-radius: 11px;
background: var(--ink); color: #faf9f5;
display: flex; align-items: center; justify-content: center;
font-family: Georgia, 'Times New Roman', serif;
font-weight: 900; font-size: 16px;
box-shadow: 0 4px 14px rgba(28, 28, 28, 0.18);
}
.brand-text { line-height: 1.1; }
.brand-name {
display: block; font-size: 14px; font-weight: 700;
font-family: Georgia, 'Times New Roman', serif;
}
.brand-sub {
display: block; font-size: 9px; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--accent); font-weight: 700;
}
.icon-btn {
width: 34px; height: 34px; border-radius: 10px;
border: 1px solid var(--border); background: #fff;
color: var(--muted); cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.icon-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-soft);
}
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
.conn-badge {
display: flex;
align-items: center;
gap: 6px;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.conn-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
}
.settings-panel {
padding: 14px 18px;
background: #fff;
border-bottom: 1px solid var(--border);
}
.settings-panel[hidden] { display: none !important; }
.settings-hint {
margin: 8px 0 0;
font-size: 11px;
color: var(--muted);
line-height: 1.5;
}
.settings-hint code {
font-size: 10px;
background: var(--paper);
padding: 1px 4px;
border-radius: 4px;
}
.settings-status {
margin: 10px 0 0;
font-size: 11px;
line-height: 1.45;
}
.settings-status.is-ok { color: #059669; }
.settings-status.is-error { color: #dc2626; }
.preset-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.preset-btn {
border: 1px solid var(--border);
background: var(--paper);
border-radius: 999px;
padding: 6px 10px;
font-size: 10px;
font-weight: 700;
cursor: pointer;
color: var(--muted);
}
.preset-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-soft);
}
.settings-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field span {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.12em;
color: var(--muted); font-weight: 700;
}
input[type="url"],
input[type="text"],
.notebook-select {
width: 100%; padding: 10px 12px; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--paper);
font-family: inherit; font-size: 12px;
}
input[type="url"]:focus,
input[type="text"]:focus,
.notebook-select:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.notebook-select {
background: #fff;
font-weight: 600;
cursor: pointer;
}
.main {
flex: 1;
padding: 16px 18px 20px;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
}
.main > .actions {
margin-top: auto;
}
.footer {
padding: 10px 18px 14px;
border-top: 1px solid var(--border);
background: #fff;
text-align: center;
}
.footer-meta { font-size: 9px; color: #9ca3af; letter-spacing: 0.06em; }
.label {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--muted); font-weight: 700; margin-bottom: 8px; display: block;
}
.auth-hint {
padding: 12px 14px;
border-radius: var(--radius);
background: #fffbeb;
border: 1px solid #fde68a;
font-size: 11px;
color: #92400e;
line-height: 1.5;
}
.page-card {
padding: 14px; border: 1px solid var(--border); border-radius: var(--radius);
background: #fff;
box-shadow: 0 1px 0 rgba(255,255,255,0.8) inset;
}
.page-card .sub {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--muted); font-weight: 700; display: block; margin-bottom: 8px;
}
.page-row { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
.page-row img {
width: 20px; height: 20px; border-radius: 5px;
flex-shrink: 0; margin-top: 2px;
}
.page-text { min-width: 0; flex: 1; }
.page-row .title {
font-size: 12px; font-weight: 700; line-height: 1.45;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
unicode-bidi: plaintext;
}
.page-row .url {
font-size: 10px; color: var(--muted); margin-top: 4px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
direction: ltr; text-align: left;
}
.text-rtl {
direction: rtl;
text-align: right;
font-family: 'Vazirmatn', 'Inter', sans-serif;
unicode-bidi: plaintext;
}
.selection-panel {
border-radius: var(--radius);
border: 1px solid var(--border);
background: #fff;
overflow: hidden;
min-height: 140px;
display: flex;
flex-direction: column;
}
.selection-panel.has-text {
border-color: #bae6fd;
background: rgba(14, 165, 233, 0.05);
box-shadow: 0 0 0 1px rgba(14, 165, 233, 0.12);
}
.selection-hint {
padding: 16px;
border: 1px dashed var(--border);
border-radius: var(--radius);
text-align: center;
background: #fff;
}
.selection-hint p {
margin: 0;
font-size: 11px;
color: var(--muted);
line-height: 1.55;
}
.selection-head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px;
background: linear-gradient(180deg, #fff 0%, #fdfcfa 100%);
border-bottom: 1px solid var(--border);
}
.selection-head .status {
display: flex; align-items: center; gap: 8px;
font-size: 10px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.08em; color: var(--muted);
}
.selection-head .status.live { color: #0284c7; }
.selection-head .count {
font-size: 10px; font-weight: 700; color: var(--muted);
background: var(--paper); padding: 4px 8px; border-radius: 999px;
}
.selection-head .count.active { color: var(--accent); background: var(--accent-soft); }
.selection-body {
flex: 1;
padding: 14px;
font-size: 13px;
line-height: 1.75;
color: rgba(28, 28, 28, 0.88);
max-height: 220px;
overflow-y: auto;
unicode-bidi: plaintext;
border-inline-start: 3px solid transparent;
}
.selection-panel.has-text .selection-body {
border-inline-start-color: #38bdf8;
padding-inline-start: 16px;
font-style: italic;
font-size: 12px;
max-height: 150px;
}
.pulse-dot {
width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
animation: pulse 1.4s ease infinite;
}
.pulse-dot.sky { background: #0ea5e9; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.85)} }
.clear-btn {
border: none; background: none; font-size: 10px;
color: var(--muted); cursor: pointer; font-weight: 600;
padding: 4px 6px; border-radius: 6px;
}
.clear-btn:hover { color: var(--ink); background: var(--paper); }
.actions {
display: flex; flex-direction: column; gap: 10px;
margin-top: auto; padding-top: 6px;
}
.btn {
padding: 14px 16px; border-radius: var(--radius); border: none; cursor: pointer;
font-weight: 700; font-size: 10px; text-transform: uppercase;
letter-spacing: 0.1em;
display: flex; align-items: center; justify-content: center; gap: 8px;
transition: transform 0.12s ease, opacity 0.12s ease, filter 0.12s ease;
}
.btn:active { transform: scale(0.98); }
.btn:disabled {
opacity: 0.42; cursor: not-allowed; transform: none;
box-shadow: none !important;
}
.btn-primary {
background: var(--ink); color: #fff;
box-shadow: 0 10px 24px rgba(28, 28, 28, 0.18);
}
.btn-primary:hover:not(:disabled) { opacity: 0.94; }
.btn-sky {
background: #0284c7;
color: #fff;
box-shadow: 0 10px 22px rgba(2, 132, 199, 0.22);
}
.btn-sky:hover:not(:disabled) { background: #0369a1; }
.btn-secondary {
background: #f3f4f6;
color: #374151;
box-shadow: none;
}
.btn-secondary:hover:not(:disabled) { background: #e5e7eb; }
.btn-sm {
padding: 10px 12px;
font-size: 10px;
}
.btn-danger { background: var(--danger); color: #fff; }
.btn-link.link-only {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-link {
background: none; border: none; color: var(--muted); font-size: 11px;
cursor: pointer; padding: 8px; font-weight: 500;
}
.btn-link:hover { color: var(--ink); text-decoration: underline; }
.btn-icon { width: 14px; height: 14px; display: inline-flex; }
.center-state {
flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; text-align: center; gap: 14px; padding: 32px 12px;
min-height: 280px;
}
.spinner-wrap { position: relative; width: 52px; height: 52px; }
.spinner-ring {
position: absolute; inset: 0; border-radius: 50%;
border: 1px solid var(--border); animation: ping 1.2s ease infinite;
}
@keyframes ping { 0%{transform:scale(1);opacity:.6} 100%{transform:scale(1.35);opacity:0} }
.spinner {
position: absolute; inset: 6px;
border: 3px solid var(--border); border-top-color: var(--accent);
border-radius: 50%; animation: spin 0.75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.state-title {
font-size: 10px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.14em; color: var(--muted);
}
.state-sub { font-size: 15px; font-weight: 700; color: var(--ink); }
.state-detail {
font-size: 11px; color: var(--muted); max-width: 280px;
line-height: 1.55; margin: 0 auto;
}
.success-icon, .error-icon {
width: 58px; height: 58px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 26px; font-weight: 700;
}
.success-icon {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.25); color: var(--success);
}
.error-icon { background: #fef2f2; color: var(--danger); }
.badge-ok {
display: inline-block; margin-bottom: 8px;
font-size: 9px; background: rgba(16, 185, 129, 0.12);
color: #059669; font-weight: 800; padding: 3px 8px; border-radius: 6px;
text-transform: uppercase; letter-spacing: 0.1em;
}
.note-title {
font-size: 15px; font-weight: 700;
font-family: Georgia, 'Times New Roman', serif;
line-height: 1.35; margin-top: 6px;
unicode-bidi: plaintext;
}
.tags {
display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
padding-top: 14px; border-top: 1px solid var(--border); margin-top: 10px;
width: 100%;
}
.tag-chip {
font-size: 9px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--accent);
background: var(--accent-soft); border: 1px solid rgba(164, 113, 72, 0.2);
padding: 5px 10px; border-radius: 999px;
}
.restricted-note {
padding: 14px; border-radius: var(--radius);
background: #fef2f2; border: 1px solid #fecaca;
font-size: 11px; color: #991b1b; line-height: 1.5;
}
.confirm-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.summary-preview {
margin: 0;
font-size: 12px;
color: var(--muted);
line-height: 1.55;
font-style: italic;
}
.excerpt-preview {
padding: 12px 14px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: #fff;
font-size: 12px;
line-height: 1.65;
max-height: 150px;
overflow-y: auto;
unicode-bidi: plaintext;
}
.excerpt-label {
display: block;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 700;
margin-bottom: 8px;
}
.meta-row { margin-top: -4px; }
.reading-time {
font-size: 10px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.preview-tags { justify-content: flex-start; border-top: none; margin-top: 0; padding-top: 0; }
html[dir="rtl"] .header,
html[dir="rtl"] .header-right,
html[dir="rtl"] .selection-head,
html[dir="rtl"] .page-row {
flex-direction: row-reverse;
}
html[dir="rtl"] .page-row .url {
direction: ltr;
text-align: left;
}
html[dir="rtl"] .notebook-select,
html[dir="rtl"] .dropdown-item,
html[dir="rtl"] .label,
html[dir="rtl"] .sub {
text-align: right;
}

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Memento Web Clipper</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Vazirmatn:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="sidepanel.css" />
</head>
<body>
<div id="app" class="shell">
<header class="header">
<div class="brand">
<div class="brand-logo">M</div>
<div class="brand-text">
<span class="brand-name">Memento</span>
<span class="brand-sub">Web Clipper</span>
</div>
</div>
<div class="header-right">
<div id="connBadge" class="conn-badge" hidden>
<span class="conn-dot"></span>
<span id="connLabel"></span>
</div>
<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>
</div>
</header>
<div id="settingsPanel" class="settings-panel" hidden>
<label class="field">
<span id="instanceUrlLabel"></span>
<input id="baseUrl" type="text" spellcheck="false" placeholder="http://localhost:3000" />
</label>
<div class="preset-row">
<button type="button" class="preset-btn" data-url="https://memento-note.com"></button>
<button type="button" class="preset-btn" data-url="http://localhost:3000">localhost:3000</button>
<button type="button" class="preset-btn" data-url="http://127.0.0.1:3000">127.0.0.1:3000</button>
</div>
<div class="settings-actions">
<button type="button" id="applyInstanceBtn" class="btn btn-primary btn-sm"></button>
<button type="button" id="openLoginBtn" class="btn btn-secondary btn-sm"></button>
</div>
<p class="settings-hint"></p>
<p id="settingsStatus" class="settings-status" hidden></p>
</div>
<main id="screen" class="main"></main>
<footer class="footer">
<span class="footer-meta"></span>
</footer>
</div>
<script src="i18n.js"></script>
<script src="sidepanel.js"></script>
</body>
</html>

View File

@@ -0,0 +1,726 @@
/** Mettre à false pour le build Chrome Web Store (URL production en dur). */
const ALLOW_INSTANCE_CONFIG = false
const DEFAULT_BASE = 'https://memento-note.com'
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
/** Compatibilité Chrome/Firefox : Firefox expose l'API sous `browser.*`,
* Chrome sous `chrome.*`. On normalise pour utiliser `chrome.*` partout dans le code. */
if (typeof chrome === 'undefined' && typeof browser !== 'undefined') {
globalThis.chrome = browser
}
let state = 'idle'
let notebooks = []
let selectedNotebookId = ''
let pageUrl = ''
let pageTitle = ''
let pageDomain = ''
let pageFavicon = ''
let pageHtml = ''
let pageDir = 'ltr'
let pageLang = ''
let selectionText = ''
let pageRestricted = false
let lastNoteId = ''
let lastNoteUrl = ''
let successTitle = ''
let successTags = []
let errorMessage = ''
let activeTabId = null
let pendingClipType = 'page'
let analyzeResult = null
let editableTitle = ''
let connected = false
const els = {
screen: document.getElementById('screen'),
baseUrl: document.getElementById('baseUrl'),
settingsPanel: document.getElementById('settingsPanel'),
settingsBtn: document.getElementById('settingsBtn'),
connBadge: document.getElementById('connBadge'),
connLabel: document.getElementById('connLabel'),
settingsStatus: document.getElementById('settingsStatus'),
applyInstanceBtn: document.getElementById('applyInstanceBtn'),
openLoginBtn: document.getElementById('openLoginBtn'),
}
const ICON_SELECT =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>'
const ICON_CLIP =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>'
const ICON_LINK =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>'
function apiBase() {
if (!ALLOW_INSTANCE_CONFIG) return DEFAULT_BASE
return (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
}
function isRestrictedUrl(url) {
return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools):/i.test(url)
}
async function ensureApiPermission() {
const origin = `${apiBase()}/*`
const has = await chrome.permissions.contains({ origins: [origin] })
if (!has) {
const granted = await chrome.permissions.request({ origins: [origin] })
if (!granted) throw new Error(t('errPermissionDenied'))
}
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
const RTL_CHAR = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
const LTR_CHAR = /[A-Za-z0-9]/
function detectTextDirection(text) {
const sample = String(text || '').replace(/\s+/g, '').slice(0, 3000)
if (!sample) return 'ltr'
let rtl = 0
let ltr = 0
for (const ch of sample) {
if (RTL_CHAR.test(ch)) rtl++
else if (LTR_CHAR.test(ch)) ltr++
}
if (rtl === 0) return 'ltr'
return rtl >= ltr ? 'rtl' : 'ltr'
}
function resolveUiDirection(text) {
if (pageDir === 'rtl') return 'rtl'
if (pageLang === 'fa' || pageLang === 'ar' || pageLang === 'he') return 'rtl'
if (/\/persian\b|\/fa\b|bbc\.com\/persian/i.test(pageUrl)) return 'rtl'
return detectTextDirection(text)
}
function rtlAttrs(text) {
if (resolveUiDirection(text) !== 'rtl') return ''
const lang = pageLang && ['fa', 'ar', 'he'].includes(pageLang) ? ` lang="${pageLang}"` : ''
return ` class="text-rtl" dir="rtl"${lang}`
}
function sortNotebooksHierarchy(list) {
const byParent = new Map()
for (const n of list) {
const pid = n.parentId || '__root__'
if (!byParent.has(pid)) byParent.set(pid, [])
byParent.get(pid).push(n)
}
for (const items of byParent.values()) {
items.sort((a, b) => (a.name || '').localeCompare(b.name || '', uiLocaleTag()))
}
const out = []
const seen = new Set()
function walk(parentKey, depth) {
for (const n of byParent.get(parentKey) || []) {
if (seen.has(n.id)) continue
seen.add(n.id)
out.push({ ...n, depth })
walk(n.id, depth + 1)
}
}
walk('__root__', 0)
for (const n of list) {
if (!seen.has(n.id)) out.push({ ...n, depth: 0 })
}
return out
}
function notebookSelectHtml() {
const sorted = sortNotebooksHierarchy(notebooks)
const opts = sorted
.map((n) => {
const indent = n.depth > 0 ? '\u00A0\u00A0'.repeat(n.depth) + '↳ ' : ''
const sel = n.id === selectedNotebookId ? ' selected' : ''
return `<option value="${escapeHtml(n.id)}"${sel}>${escapeHtml(indent + (n.name || t('notebookUnnamed')))}</option>`
})
.join('')
return `<select id="notebookSelect" class="notebook-select" aria-label="${escapeHtml(t('destinationNotebook'))}">
${notebooks.length ? opts : `<option value="">${escapeHtml(t('noNotebooks'))}</option>`}
</select>`
}
function formatReadingTime(minutes) {
const m = Number(minutes) || 0
if (m <= 0) return ''
if (m === 1) return t('readingTimeOne')
return t('readingTimeOther', String(m))
}
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
return tab
}
async function ensureContentScript(tabId) {
try {
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
if (resp?.ok) return true
} catch {
/* inject */
}
try {
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
return true
} catch {
return false
}
}
async function setPickModeOnTab(enabled) {
if (!activeTabId || pageRestricted) return
const ok = await ensureContentScript(activeTabId)
if (!ok) return
try {
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
} catch {
/* ignore */
}
}
async function syncPickMode() {
await setPickModeOnTab(state === 'idle' && !pageRestricted)
}
function updateConnBadge() {
if (!els.connBadge) return
els.connBadge.hidden = !connected
if (els.connLabel) els.connLabel.textContent = connected ? t('connected') : t('disconnected')
}
function setSettingsStatus(msg, isError) {
if (!els.settingsStatus) return
els.settingsStatus.hidden = !msg
els.settingsStatus.textContent = msg || ''
els.settingsStatus.className = `settings-status${isError ? ' is-error' : ' is-ok'}`
}
function applyInstanceConfigVisibility() {
if (ALLOW_INSTANCE_CONFIG) return
els.settingsPanel?.setAttribute('hidden', '')
els.settingsBtn?.setAttribute('hidden', '')
if (els.baseUrl) els.baseUrl.value = DEFAULT_BASE
}
function selectionBlockHtml() {
if (selectionText) {
return `<div class="selection-panel has-text" id="selectionSlot">
<div class="selection-head">
<span class="status live"><span class="pulse-dot sky"></span> ${escapeHtml(t('selectionDetected'))}</span>
<button type="button" class="clear-btn" id="clearSel">${escapeHtml(t('ignore'))}</button>
</div>
<div class="selection-body"${rtlAttrs(selectionText)}>「 ${escapeHtml(selectionText)} 」</div>
</div>`
}
return `<div class="selection-hint" id="selectionSlot">
<p>${escapeHtml(t('selectionHint'))}</p>
</div>`
}
function actionsBlockHtml() {
const hasSel = Boolean(selectionText)
return `<div class="actions" id="actionsSlot">
${
hasSel
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
${ICON_SELECT} ${escapeHtml(t('clipSelection'))}
</button>`
: ''
}
<button type="button" class="btn ${hasSel ? 'btn-secondary' : 'btn-primary'}" id="clipPageBtn" ${pageRestricted ? 'disabled' : ''}>
${ICON_CLIP} ${escapeHtml(t('clipPage'))}
</button>
<button type="button" class="btn-link link-only" id="clipLinkBtn" ${pageRestricted ? 'disabled' : ''}>
${ICON_LINK} ${escapeHtml(t('saveLinkOnly'))}
</button>
</div>`
}
function bindIdleHandlers() {
document.getElementById('notebookSelect')?.addEventListener('change', async (e) => {
selectedNotebookId = e.target.value || ''
await chrome.storage.sync.set({ [STORAGE_KEYS.notebookId]: selectedNotebookId })
})
document.getElementById('clearSel')?.addEventListener('click', () => void clearSelection())
document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection'))
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) {
this.src = fallback
}
})
}
async function clearSelection() {
selectionText = ''
if (activeTabId) {
try {
await chrome.scripting.executeScript({
target: { tabId: activeTabId },
func: () => window.getSelection()?.removeAllRanges(),
})
} catch {
/* ignore */
}
}
updateSelectionUI()
}
function updateSelectionUI() {
const slot = document.getElementById('selectionSlot')
const actions = document.getElementById('actionsSlot')
if (!slot || !actions || state !== 'idle') {
render()
return
}
slot.outerHTML = selectionBlockHtml()
actions.outerHTML = actionsBlockHtml()
bindIdleHandlers()
}
function applySelectionFromMessage(msg) {
if (!msg || msg.url !== pageUrl) return
selectionText = msg.text || ''
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
if (msg.lang) pageLang = msg.lang
if (state === 'idle') updateSelectionUI()
}
async function refreshPageContext() {
const tab = await getActiveTab()
activeTabId = tab?.id ?? null
pageRestricted = isRestrictedUrl(tab?.url)
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>`
: ''
const authHint =
!connected && errorMessage
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
: ''
els.screen.innerHTML = `
${restrictedBlock}
${authHint}
<div>
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
${notebookSelectHtml()}
</div>
<div class="page-card">
<span class="sub">${escapeHtml(t('activePage'))}</span>
<div class="page-row">
<img src="${escapeHtml(pageFavicon)}" alt="" class="page-favicon" data-fallback="https://www.google.com/s2/favicons?domain=google.com&sz=32" />
<div class="page-text">
<div class="title"${rtlAttrs(pageTitle)}>${escapeHtml(pageTitle || '—')}</div>
<div class="url">${escapeHtml(pageUrl || '—')}</div>
</div>
</div>
</div>
${selectionBlockHtml()}
${actionsBlockHtml()}
`
bindIdleHandlers()
}
function renderLoading(label) {
els.screen.innerHTML = `
<div class="center-state">
<div class="spinner-wrap">
<div class="spinner-ring"></div>
<div class="spinner"></div>
</div>
<div>
<div class="state-title">${escapeHtml(t('analyzingSource'))}</div>
<div class="state-sub">${escapeHtml(label || t('statusAnalyzing'))}</div>
<div class="state-detail">${escapeHtml(t('processingDetail'))}</div>
</div>
</div>
`
}
function renderConfirm() {
const excerpt = analyzeResult?.excerpt || ''
const tags = analyzeResult?.tags || []
const reading = formatReadingTime(analyzeResult?.readingTime)
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
els.screen.innerHTML = `
<div class="confirm-panel">
<span class="label">${escapeHtml(t('previewBeforeSave'))}</span>
<label class="field">
<span>${escapeHtml(t('noteTitleLabel'))}</span>
<input id="titleInput" type="text" value="${escapeHtml(editableTitle)}" maxlength="300" />
</label>
${
reading
? `<div class="meta-row"><span class="reading-time">${escapeHtml(reading)}</span></div>`
: ''
}
${
analyzeResult?.summary
? `<p class="summary-preview"${rtlAttrs(analyzeResult.summary)}>${escapeHtml(analyzeResult.summary)}</p>`
: ''
}
${
excerpt && pendingClipType !== 'link'
? `<div class="excerpt-preview"${rtlAttrs(excerpt)}>
<span class="excerpt-label">${escapeHtml(t('excerptLabel'))}</span>
${escapeHtml(excerpt)}
</div>`
: ''
}
${tagsHtml ? `<div class="tags preview-tags">${tagsHtml}</div>` : ''}
</div>
<div class="actions">
<button type="button" class="btn btn-primary" id="saveBtn">${escapeHtml(t('saveToMomento'))}</button>
<button type="button" class="btn-link" id="cancelConfirmBtn">${escapeHtml(t('back'))}</button>
</div>
`
document.getElementById('titleInput')?.addEventListener('input', (e) => {
editableTitle = e.target.value
})
document.getElementById('saveBtn')?.addEventListener('click', () => void runSave())
document.getElementById('cancelConfirmBtn')?.addEventListener('click', async () => {
state = 'idle'
analyzeResult = null
await syncPickMode()
render()
})
}
function renderSuccess() {
const nb = notebooks.find((n) => n.id === selectedNotebookId)
const tagsHtml = successTags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
const reading = formatReadingTime(analyzeResult?.readingTime)
els.screen.innerHTML = `
<div class="center-state" style="justify-content:flex-start;padding-top:12px">
<div class="success-icon">✓</div>
<div>
<span class="badge-ok">${escapeHtml(t('noteSaved'))}</span>
<div class="note-title"${rtlAttrs(successTitle)}>${escapeHtml(successTitle)}</div>
<div class="state-detail">${escapeHtml(t('sentToNotebook', nb?.name || ''))}</div>
${reading ? `<div class="state-detail">${escapeHtml(reading)}</div>` : ''}
</div>
${tagsHtml ? `<div class="tags">${tagsHtml}</div>` : ''}
</div>
<div class="actions">
<button type="button" class="btn btn-primary" id="viewBtn">${escapeHtml(t('viewInMomento'))} ↗</button>
<button type="button" class="btn-link" id="againBtn">${escapeHtml(t('clipAnother'))}</button>
</div>
`
document.getElementById('viewBtn')?.addEventListener('click', () => {
if (lastNoteUrl) chrome.tabs.create({ url: `${apiBase()}${lastNoteUrl}` })
})
document.getElementById('againBtn')?.addEventListener('click', async () => {
state = 'idle'
analyzeResult = null
await refreshPageContext()
await syncPickMode()
render()
})
}
function renderError() {
els.screen.innerHTML = `
<div class="center-state">
<div class="error-icon">!</div>
<div>
<div class="state-title" style="color:#ef4444">${escapeHtml(t('failure'))}</div>
<div class="state-detail">${escapeHtml(errorMessage || t('genericError'))}</div>
</div>
</div>
<div class="actions">
<button type="button" class="btn btn-danger" id="retryBtn">${escapeHtml(t('retry'))}</button>
<button type="button" class="btn-link" id="backIdleBtn">${escapeHtml(t('back'))}</button>
</div>
`
document.getElementById('retryBtn')?.addEventListener('click', () => {
if (analyzeResult) void runSave()
else void runAnalyze(pendingClipType)
})
document.getElementById('backIdleBtn')?.addEventListener('click', async () => {
state = 'idle'
errorMessage = ''
analyzeResult = null
await refreshPageContext()
await syncPickMode()
render()
})
}
function render() {
if (state === 'loading' || state === 'saving') {
return renderLoading(state === 'saving' ? t('statusSaving') : t('statusAnalyzing'))
}
if (state === 'confirm') return renderConfirm()
if (state === 'success') return renderSuccess()
if (state === 'error') return renderError()
renderIdle()
}
async function runAnalyze(type) {
pendingClipType = type
state = 'loading'
await setPickModeOnTab(false)
render()
try {
await ensureApiPermission()
await chrome.storage.sync.set({
[STORAGE_KEYS.baseUrl]: apiBase(),
[STORAGE_KEYS.notebookId]: selectedNotebookId,
})
if (type === 'selection') {
if (!selectionText) throw new Error(t('errNoSelection'))
await refreshPageContext()
}
let analyzeBody
if (type === 'link') {
analyzeBody = { url: pageUrl, title: pageTitle, mode: 'link' }
} else if (type === 'selection' && selectionText) {
analyzeBody = { url: pageUrl, title: pageTitle, mode: 'selection', selection: selectionText }
} else {
analyzeBody = { url: pageUrl, html: pageHtml, title: pageTitle, mode: 'article' }
}
const analyzeRes = await fetch(`${apiBase()}/api/clip/analyze`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(analyzeBody),
})
const analysis = await analyzeRes.json()
if (!analyzeRes.ok) throw new Error(analysis.error || t('errAnalyzeFailed'))
analyzeResult = analysis
editableTitle = analysis.title || pageTitle || pageDomain
state = 'confirm'
render()
} catch (e) {
errorMessage = e.message || t('errNetwork')
state = 'error'
render()
}
}
async function runSave() {
if (!analyzeResult) return
state = 'saving'
render()
try {
const title = (editableTitle || analyzeResult.title || pageTitle || pageDomain).trim()
const saveRes = await fetch(`${apiBase()}/api/clip/save`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: pageUrl,
title,
content: analyzeResult.content,
summary: analyzeResult.summary,
tags: analyzeResult.tags || [],
notebookId: selectedNotebookId || undefined,
}),
})
const saved = await saveRes.json()
if (!saveRes.ok) throw new Error(saved.error || t('errSaveFailed'))
successTitle = title
successTags = analyzeResult.tags || []
lastNoteId = saved.noteId
lastNoteUrl = saved.noteUrl
state = 'success'
render()
} catch (e) {
errorMessage = e.message || t('errNetwork')
state = 'error'
render()
}
}
chrome.runtime.onMessage.addListener((msg) => {
if (msg?.type === 'SELECTION_CHANGED') applySelectionFromMessage(msg)
})
chrome.tabs.onActivated.addListener(async () => {
if (state !== 'idle') return
await refreshPageContext()
await syncPickMode()
render()
})
chrome.tabs.onUpdated.addListener(async (tabId, info) => {
if (info.status !== 'complete' || state !== 'idle') return
const tab = await getActiveTab()
if (tab?.id === tabId) {
await refreshPageContext()
await syncPickMode()
render()
}
})
els.settingsBtn?.addEventListener('click', () => {
if (!ALLOW_INSTANCE_CONFIG) return
els.settingsPanel.hidden = !els.settingsPanel.hidden
})
document.querySelectorAll('.preset-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const url = btn.getAttribute('data-url')
if (url && els.baseUrl) els.baseUrl.value = url
})
})
els.applyInstanceBtn?.addEventListener('click', () => void applyInstance())
els.openLoginBtn?.addEventListener('click', () => {
chrome.tabs.create({ url: apiBase() })
})
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
if (state === 'idle') {
await refreshPageContext()
await syncPickMode()
render()
}
} else if (document.visibilityState === 'hidden') {
// Désactiver le pick mode quand le sidepanel est fermé
await setPickModeOnTab(false)
}
})
document.addEventListener('DOMContentLoaded', async () => {
applyDocumentLocale()
applyInstanceConfigVisibility()
applyShellI18n()
await loadSettings()
try {
await ensureApiPermission()
} catch (e) {
errorMessage = e.message
connected = false
updateConnBadge()
}
await refreshPageContext()
await syncPickMode()
render()
})

View File

@@ -6,191 +6,289 @@ const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const VERSION = "0.3.1"
const VERSION = "0.4.0"
// Embedded Momento clipper/extension string table (gzip+base64).
// Source English + contextual French anchors; other locales are MT batch
// post-edited with placeholder hardening.
const PACK_B64 = `
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
wOW7jPkAAA==
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
`
function loadStrings () {

View File

@@ -1,5 +1,5 @@
{
"version": "0.3.1",
"version": "0.4.0",
"strings": {
"en": {
"extName": {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "0.3.1",
"version": "0.4.0",
"description": "__MSG_extDescription__",
"default_locale": "en",
"permissions": ["activeTab", "scripting", "storage", "sidePanel", "tabs"],
@@ -12,12 +12,25 @@
"http://*/*",
"https://*/*"
],
"optional_host_permissions": [
"https://*/*"
],
"background": {
"service_worker": "background.js"
},
"side_panel": {
"default_path": "sidepanel.html"
},
"sidebar_action": {
"default_title": "__MSG_extActionTitle__",
"default_panel": "sidepanel.html"
},
"browser_specific_settings": {
"gecko": {
"id": "memento-clipper@memento-note.com",
"strict_min_version": "115.0"
}
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],

Binary file not shown.

View File

@@ -40,17 +40,24 @@ function ensureDir(dirPath) {
}
}
// Copy all files from source to destination, excluding specified patterns
// Copy all files from source to destination, excluding specified patterns.
// `exclude` accepts RegExp (matched against the relative path) or a function
// returning true when the entry (relative path, isDirectory flag) should be skipped.
function copyFiles(src, dest, exclude = []) {
ensureDir(dest)
const entries = fs.readdirSync(src, { withFileTypes: true })
const isExcluded = (relPath, isDir) =>
exclude.some(rule => {
if (typeof rule === 'function') return rule(relPath, isDir)
return relPath.match(rule) !== null
})
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const relPath = path.relative(extRoot, srcPath)
// Skip excluded files/directories
if (exclude.some(pattern => relPath.match(pattern))) {
if (isExcluded(relPath, entry.isDirectory())) {
continue
}
@@ -85,6 +92,11 @@ function processManifestJson(content) {
manifest.content_scripts[0].matches = ['https://memento-note.com/*', 'https://www.memento-note.com/*']
}
// Strip Firefox-only fields (Chrome Store rejects unknown keys on strict review)
delete manifest.sidebar_action
delete manifest.browser_specific_settings
delete manifest.optional_host_permissions
return JSON.stringify(manifest, null, 2)
}
@@ -177,13 +189,19 @@ async function build() {
}
ensureDir(distDir)
// Copy extension files (excluding build scripts and dist)
// Copy extension files (excluding build scripts, internal sources and dist)
log('📋 Copying extension files...', 'blue')
copyFiles(extRoot, distDir, [
/^dist-/,
/^scripts\//,
// Top-level directories to skip entirely (do not even create empty folders)
(rel, isDir) => isDir && /^dist-/i.test(rel),
(rel, isDir) => isDir && (rel === 'scripts' || rel === 'i18n'),
// File-level patterns
/\.md$/,
/^node_modules$/
/^node_modules$/,
/^diagnose\.js$/, // dev diagnostic script
/^test-sidepanel\.html$/, // dev test page
/^memento-web-clipper-chrome-store\.zip$/, // avoid recursive zip-in-zip
/^memento-web-clipper-firefox\.xpi$/, // exclude sibling Firefox build artifacts
])
// Process sidepanel.js

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env node
/**
* Build script for Firefox Add-ons (AMO) production package
* Usage: node scripts/build-firefox.mjs
*
* This script:
* 1. Sets ALLOW_INSTANCE_CONFIG = false in sidepanel.js
* 2. Restricts host_permissions to memento-note.com only
* 3. Removes side_panel (Chrome-only) from manifest.json
* 4. Keeps sidebar_action + browser_specific_settings (Firefox-specific)
* 5. Copies and generates icons from public/icons/
* 6. Creates a Firefox-ready .xpi package
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import AdmZip from 'adm-zip'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const extRoot = path.resolve(__dirname, '..')
const projectRoot = path.resolve(extRoot, '..')
const publicIconsDir = path.join(projectRoot, 'public', 'icons')
const distDir = path.join(extRoot, 'dist-firefox')
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m'
}
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
}
// Copy all files from source to destination, excluding specified patterns.
// `exclude` accepts RegExp or a function (relPath, isDirectory) -> boolean.
function copyFiles(src, dest, exclude = []) {
ensureDir(dest)
const entries = fs.readdirSync(src, { withFileTypes: true })
const isExcluded = (relPath, isDir) =>
exclude.some(rule => {
if (typeof rule === 'function') return rule(relPath, isDir)
return relPath.match(rule) !== null
})
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const relPath = path.relative(extRoot, srcPath)
if (isExcluded(relPath, entry.isDirectory())) {
continue
}
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
copyFiles(srcPath, destPath, exclude)
} else {
fs.copyFileSync(srcPath, destPath)
}
}
}
// Read and modify sidepanel.js for production (lock URL to memento-note.com)
function processSidepanelJs(content) {
return content.replace(
/const ALLOW_INSTANCE_CONFIG = true/,
'const ALLOW_INSTANCE_CONFIG = false'
)
}
// Read and modify manifest.json for Firefox production
function processManifestJson(content) {
const manifest = JSON.parse(content)
// Firefox doesn't support chrome.sidePanel — use sidebarAction (already declared).
delete manifest.side_panel
// Firefox MV3 service workers are supported, but background events are also
// reliable with background.scripts. Keep service_worker for simplicity.
// Restrict host_permissions to production. Keep optional_host_permissions as-is.
manifest.host_permissions = [
'https://memento-note.com/*',
'https://www.memento-note.com/*',
]
if (manifest.content_scripts?.[0]) {
manifest.content_scripts[0].matches = ['https://memento-note.com/*', 'https://www.memento-note.com/*']
}
return JSON.stringify(manifest, null, 2)
}
// Generate PNG icons from SVG using sharp
async function generateIcons() {
log('📦 Generating PNG icons from SVG...', 'blue')
const sharp = (await import('sharp')).default
const sizes = [16, 48, 128]
const icon512Svg = path.join(publicIconsDir, 'icon-512.svg')
const icon192Svg = path.join(publicIconsDir, 'icon-192.svg')
if (!fs.existsSync(icon512Svg) || !fs.existsSync(icon192Svg)) {
log('⚠️ Source SVG icons not found. Copying SVG files only.', 'yellow')
fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg'))
fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg'))
return
}
for (const size of sizes) {
const sourceSvg = size >= 128 ? icon512Svg : icon192Svg
const outputPath = path.join(distDir, `icon-${size}.png`)
await sharp(sourceSvg)
.resize(size, size)
.png()
.toFile(outputPath)
log(` ✓ Generated icon-${size}.png`, 'green')
}
fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg'))
fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg'))
}
// Create XPI (ZIP) package using AdmZip
async function createXpiPackage() {
log('📦 Creating XPI package...', 'blue')
const xpiPath = path.join(extRoot, 'memento-web-clipper-firefox.xpi')
try {
const zip = new AdmZip()
const addFiles = (dir, base = '') => {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
const relativePath = path.join(base, entry.name)
if (entry.isDirectory()) {
addFiles(fullPath, relativePath)
} else {
zip.addLocalFile(fullPath, base)
}
}
}
addFiles(distDir)
zip.writeZip(xpiPath)
const stats = fs.statSync(xpiPath)
log(`✓ XPI package created: ${xpiPath}`, 'green')
log(` Size: ${(stats.size / 1024).toFixed(2)} KB`, 'green')
} catch (error) {
throw new Error(`Failed to create XPI: ${error.message}`)
}
}
async function build() {
log('🦊 Starting Firefox (AMO) build...', 'blue')
log('')
try {
log('🧹 Cleaning dist directory...', 'blue')
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true })
}
ensureDir(distDir)
log('📋 Copying extension files...', 'blue')
copyFiles(extRoot, distDir, [
(rel, isDir) => isDir && /^dist-/i.test(rel),
(rel, isDir) => isDir && (rel === 'scripts' || rel === 'i18n'),
/\.md$/,
/^node_modules$/,
/^diagnose\.js$/,
/^test-sidepanel\.html$/,
/^memento-web-clipper-chrome-store\.zip$/,
/^memento-web-clipper-firefox\.xpi$/,
])
log('⚙️ Processing sidepanel.js...', 'blue')
const sidepanelPath = path.join(distDir, 'sidepanel.js')
let sidepanelContent = fs.readFileSync(sidepanelPath, 'utf8')
sidepanelContent = processSidepanelJs(sidepanelContent)
fs.writeFileSync(sidepanelPath, sidepanelContent)
log(' ✓ Set ALLOW_INSTANCE_CONFIG = false', 'green')
log('⚙️ Processing manifest.json...', 'blue')
const manifestPath = path.join(distDir, 'manifest.json')
let manifestContent = fs.readFileSync(manifestPath, 'utf8')
manifestContent = processManifestJson(manifestContent)
const manifest = JSON.parse(manifestContent)
manifest.icons = {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
// Firefox uses browser_action icon OR sidebar_action icon
manifest.action = {
...manifest.action,
"default_icon": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
}
if (manifest.sidebar_action) {
manifest.sidebar_action.default_icon = {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
}
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
log(' ✓ Removed side_panel (Chrome-only)', 'green')
log(' ✓ Restricted host_permissions to memento-note.com', 'green')
log(' ✓ Kept sidebar_action + browser_specific_settings for Firefox', 'green')
log(' ✓ Added icon definitions', 'green')
await generateIcons()
await createXpiPackage()
log('')
log('✅ Build completed successfully!', 'green')
log('')
log('📦 Output files:', 'blue')
log(` • Package: ${path.join(extRoot, 'memento-web-clipper-firefox.xpi')}`, 'reset')
log(` • Dist dir: ${distDir}`, 'reset')
log('')
log('📝 Next steps:', 'blue')
log(' 1. Test: web-ext run --source-dir ' + distDir, 'reset')
log(' 2. Sign: web-ext sign --api-key=<KEY> --api-secret=<SECRET> --source-dir ' + distDir, 'reset')
log(' 3. Submit signed .xpi to https://addons.mozilla.org/developers/', 'reset')
log('')
} catch (error) {
log('')
log('❌ Build failed!', 'red')
log(` Error: ${error.message}`, 'red')
process.exit(1)
}
}
build()

View File

@@ -3,6 +3,12 @@ const ALLOW_INSTANCE_CONFIG = true
const DEFAULT_BASE = 'https://memento-note.com'
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
/** Compatibilité Chrome/Firefox : Firefox expose l'API sous `browser.*`,
* Chrome sous `chrome.*`. On normalise pour utiliser `chrome.*` partout dans le code. */
if (typeof chrome === 'undefined' && typeof browser !== 'undefined') {
globalThis.chrome = browser
}
let state = 'idle'
let notebooks = []
let selectedNotebookId = ''

View File

@@ -3326,9 +3326,9 @@
"link0": "الخصوصية",
"link1": "الشروط",
"link2": "ملفات تعريف الارتباط",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Datenschutz",
"link1": "Bedingungen",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -1277,7 +1277,17 @@
"under": "Under",
"trashCascadeWarning": "This will also delete all notes and sub-notebooks.",
"trashConfirm": "Move this notebook to trash?",
"trashTitle": "Move to trash"
"trashTitle": "Move to trash",
"slides": "Presentation",
"slidesDescription": "Generate a slide deck from all notes in this notebook. Images are automatically included.",
"slidesGenerate": "Generate presentation",
"slidesGenerating": "Generating presentation from notes...",
"slidesGeneratingHint": "This may take 30-60 seconds (outline + per-slide writing)",
"slidesReady": "Presentation generated",
"slidesStatus1": "Analyzing notebook notes…",
"slidesStatus2": "Building presentation outline…",
"slidesStatus3": "Writing slides…",
"slidesStatus4": "Finalizing layout…"
},
"notebookSuggestion": {
"title": "Move to {name}?",
@@ -3593,9 +3603,9 @@
"link0": "Privacy",
"link1": "Terms",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Privacidad",
"link1": "Términos",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "حریم خصوصی",
"link1": "شرایط",
"link2": "کوکی‌ها",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -1283,7 +1283,17 @@
"under": "Sous",
"trashCascadeWarning": "Cela supprimera également toutes les notes et sous-carnets.",
"trashConfirm": "Déplacer ce carnet vers la corbeille ?",
"trashTitle": "Déplacer vers la corbeille"
"trashTitle": "Déplacer vers la corbeille",
"slides": "Présentation",
"slidesDescription": "Génère une présentation à partir des notes du carnet. Les images sont automatiquement intégrées.",
"slidesGenerate": "Générer la présentation",
"slidesGenerating": "Génération de la présentation à partir des notes...",
"slidesGeneratingHint": "Cela peut prendre 30 à 60 secondes (outline + rédaction slide par slide)",
"slidesReady": "Présentation générée",
"slidesStatus1": "Analyse des notes du carnet…",
"slidesStatus2": "Création du plan de la présentation…",
"slidesStatus3": "Rédaction des slides…",
"slidesStatus4": "Finalisation et mise en page…"
},
"notebookSuggestion": {
"title": "Déplacer vers {name} ?",
@@ -3599,9 +3609,9 @@
"link0": "Confidentialité",
"link1": "Conditions",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "गोपनीयता",
"link1": "शर्तें",
"link2": "कुकीज़",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Privacy",
"link1": "Termini",
"link2": "Cookie",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "プライバシー",
"link1": "利用規約",
"link2": "Cookie",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "개인정보",
"link1": "약관",
"link2": "쿠키",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Privacy",
"link1": "Voorwaarden",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Prywatność",
"link1": "Regulamin",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Privacidade",
"link1": "Termos",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "Конфиденциальность",
"link1": "Условия",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},

View File

@@ -3326,9 +3326,9 @@
"link0": "隐私",
"link1": "条款",
"link2": "Cookies",
"link0Href": "#",
"link1Href": "#",
"link2Href": "#"
"link0Href": "/privacy",
"link1Href": "/privacy",
"link2Href": "/privacy"
}
}
},