- Sélection détectée
-
+ ${escapeHtml(t('selectionDetected'))}
+
「 ${escapeHtml(selectionText)} 」
`
}
return `Cette page ne peut pas être clippée (page système Chrome). Ouvrez un site web normal.
`
+ ? `
-
Page active
+
${escapeHtml(t('activePage'))}
@@ -432,9 +432,9 @@ function renderLoading(label) {
-
Analyse de la source
-
${escapeHtml(label || 'Traitement en cours…')}
-
Résumé, tags et préparation de la note Momento.
+
${escapeHtml(t('analyzingSource'))}
+
${escapeHtml(label || t('statusAnalyzing'))}
+
${escapeHtml(t('processingDetail'))}
`
@@ -448,9 +448,9 @@ function renderConfirm() {
els.screen.innerHTML = `
-
Aperçu avant enregistrement
+
${escapeHtml(t('previewBeforeSave'))}
${
@@ -466,7 +466,7 @@ function renderConfirm() {
${
excerpt && pendingClipType !== 'link'
? `
- Extrait
+ ${escapeHtml(t('excerptLabel'))}
${escapeHtml(excerpt)}
`
: ''
@@ -474,8 +474,8 @@ function renderConfirm() {
${tagsHtml ? `
${tagsHtml}
` : ''}
-
-
+
+
`
@@ -500,16 +500,16 @@ function renderSuccess() {
✓
-
Note enregistrée
+
${escapeHtml(t('noteSaved'))}
${escapeHtml(successTitle)}
-
Carnet « ${escapeHtml(nb?.name || '')} »
+
${escapeHtml(t('sentToNotebook', nb?.name || ''))}
${reading ? `
${escapeHtml(reading)}
` : ''}
${tagsHtml ? `
${tagsHtml}
` : ''}
-
-
+
+
`
document.getElementById('viewBtn')?.addEventListener('click', () => {
@@ -529,13 +529,13 @@ function renderError() {
!
-
Échec
-
${escapeHtml(errorMessage || 'Une erreur s\'est produite.')}
+
${escapeHtml(t('failure'))}
+
${escapeHtml(errorMessage || t('genericError'))}
-
-
+
+
`
document.getElementById('retryBtn')?.addEventListener('click', () => {
@@ -553,7 +553,9 @@ function renderError() {
}
function render() {
- if (state === 'loading' || state === 'saving') return renderLoading(state === 'saving' ? 'Enregistrement…' : 'Analyse…')
+ 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()
@@ -573,7 +575,7 @@ async function runAnalyze(type) {
})
if (type === 'selection') {
- if (!selectionText) throw new Error('Aucune sélection active.')
+ if (!selectionText) throw new Error(t('errNoSelection'))
await refreshPageContext()
}
@@ -593,14 +595,14 @@ async function runAnalyze(type) {
body: JSON.stringify(analyzeBody),
})
const analysis = await analyzeRes.json()
- if (!analyzeRes.ok) throw new Error(analysis.error || 'Analyse impossible')
+ 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 || 'Erreur réseau'
+ errorMessage = e.message || t('errNetwork')
state = 'error'
render()
}
@@ -626,7 +628,7 @@ async function runSave() {
}),
})
const saved = await saveRes.json()
- if (!saveRes.ok) throw new Error(saved.error || 'Enregistrement impossible')
+ if (!saveRes.ok) throw new Error(saved.error || t('errSaveFailed'))
successTitle = title
successTags = analyzeResult.tags || []
@@ -635,7 +637,7 @@ async function runSave() {
state = 'success'
render()
} catch (e) {
- errorMessage = e.message || 'Erreur réseau'
+ errorMessage = e.message || t('errNetwork')
state = 'error'
render()
}
@@ -688,7 +690,9 @@ document.addEventListener('visibilitychange', async () => {
})
document.addEventListener('DOMContentLoaded', async () => {
+ applyDocumentLocale()
applyInstanceConfigVisibility()
+ applyShellI18n()
await loadSettings()
try {
await ensureApiPermission()
diff --git a/memento-note/lib/flashcards/deck-queries.ts b/memento-note/lib/flashcards/deck-queries.ts
new file mode 100644
index 0000000..af52b1a
--- /dev/null
+++ b/memento-note/lib/flashcards/deck-queries.ts
@@ -0,0 +1,90 @@
+import prisma from '@/lib/prisma'
+import { isCardMastered } from '@/lib/flashcards/sm2'
+
+export interface DeckSummary {
+ id: string
+ name: string
+ notebookId: string | null
+ totalCards: number
+ dueCount: number
+ masteredCount: number
+ lastReviewedAt: string | null
+ createdAt: string
+}
+
+export async function listDeckSummaries(userId: string): Promise
{
+ const now = new Date()
+ const decks = await prisma.flashcardDeck.findMany({
+ where: { userId },
+ include: {
+ flashcards: {
+ select: {
+ id: true,
+ interval: true,
+ nextReviewAt: true,
+ reviews: {
+ orderBy: { reviewedAt: 'desc' },
+ take: 1,
+ select: { reviewedAt: true },
+ },
+ },
+ },
+ },
+ orderBy: { updatedAt: 'desc' },
+ })
+
+ return decks.map((deck) => {
+ const totalCards = deck.flashcards.length
+ const dueCount = deck.flashcards.filter((c) => c.nextReviewAt <= now).length
+ const masteredCount = deck.flashcards.filter((c) => isCardMastered(c.interval)).length
+ const lastReview = deck.flashcards
+ .flatMap((c) => c.reviews.map((r) => r.reviewedAt))
+ .sort((a, b) => b.getTime() - a.getTime())[0]
+
+ return {
+ id: deck.id,
+ name: deck.name,
+ notebookId: deck.notebookId,
+ totalCards,
+ dueCount,
+ masteredCount,
+ lastReviewedAt: lastReview ? lastReview.toISOString() : null,
+ createdAt: deck.createdAt.toISOString(),
+ }
+ })
+}
+
+export async function getDeckDetail(userId: string, deckId: string) {
+ const deck = await prisma.flashcardDeck.findFirst({
+ where: { id: deckId, userId },
+ include: {
+ flashcards: {
+ orderBy: { nextReviewAt: 'asc' },
+ include: {
+ note: { select: { id: true, title: true } },
+ },
+ },
+ },
+ })
+ if (!deck) return null
+
+ const now = new Date()
+ return {
+ id: deck.id,
+ name: deck.name,
+ notebookId: deck.notebookId,
+ cards: deck.flashcards.map((c) => ({
+ id: c.id,
+ front: c.front,
+ back: c.back,
+ type: c.type,
+ interval: c.interval,
+ easinessFactor: c.easinessFactor,
+ nextReviewAt: c.nextReviewAt.toISOString(),
+ noteId: c.noteId,
+ noteTitle: c.note?.title ?? null,
+ due: c.nextReviewAt <= now,
+ mastered: isCardMastered(c.interval),
+ })),
+ }
+}
diff --git a/memento-note/lib/flashcards/deck-utils.ts b/memento-note/lib/flashcards/deck-utils.ts
new file mode 100644
index 0000000..86e8842
--- /dev/null
+++ b/memento-note/lib/flashcards/deck-utils.ts
@@ -0,0 +1,55 @@
+import prisma from '@/lib/prisma'
+
+export async function getOrCreateDeckForNotebook(params: {
+ userId: string
+ notebookId: string | null
+ notebookName?: string | null
+ manualName?: string
+}) {
+ const { userId, notebookId, notebookName, manualName } = params
+
+ if (notebookId) {
+ const existing = await prisma.flashcardDeck.findFirst({
+ where: { userId, notebookId },
+ })
+ if (existing) return existing
+
+ const notebook = await prisma.notebook.findFirst({
+ where: { id: notebookId, userId },
+ select: { name: true },
+ })
+ if (!notebook) {
+ throw new Error('Notebook not found')
+ }
+
+ return prisma.flashcardDeck.create({
+ data: {
+ userId,
+ notebookId,
+ name: notebook.name,
+ },
+ })
+ }
+
+ const name = (manualName || notebookName || 'Deck').trim().slice(0, 120)
+ if (!name) {
+ throw new Error('Deck name required')
+ }
+
+ return prisma.flashcardDeck.create({
+ data: {
+ userId,
+ name,
+ },
+ })
+}
+
+export function stripHtmlToText(html: string): string {
+ return html
+ .replace(/