diff --git a/main.js b/main.js index 9f7e23c..9398223 100644 --- a/main.js +++ b/main.js @@ -32598,7 +32598,7 @@ var PseudonymizationView = class extends import_obsidian7.ItemView { else if (tab === "mappings") await this.renderMappingsTab(pane); else if (tab === "dictionaries") - this.renderDictionariesTab(pane); + await this.renderDictionariesTab(pane); else if (tab === "ner") await this.renderNerTab(pane); else @@ -32893,15 +32893,90 @@ var PseudonymizationView = class extends import_obsidian7.ItemView { } } // ---- Onglet Dictionnaires -------------------------------------- - renderDictionariesTab(el) { - el.createEl("p", { - text: "La gestion avanc\xE9e des dictionnaires (d\xE9tection NER, listes de lieux et d'institutions) sera disponible en Phase 9.", - cls: "pseudobs-view-hint" + async renderDictionariesTab(el) { + const toolbar = el.createDiv("pseudobs-view-toolbar"); + const importBtn = toolbar.createEl("button", { + text: "Importer un dictionnaire (.dict.json)", + cls: "pseudobs-view-add-btn" }); - el.createEl("p", { - text: "Utilisez l'outil Coulmont via le menu contextuel (clic droit sur un pr\xE9nom dans la transcription).", - cls: "pseudobs-view-hint" + const statusEl = toolbar.createSpan({ cls: "pseudobs-view-dict-status" }); + importBtn.addEventListener("click", () => { + const input = activeDocument.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.multiple = true; + input.classList.add("pseudobs-hidden-input"); + activeDocument.body.appendChild(input); + input.addEventListener("change", () => { + void (async () => { + const files2 = Array.from(input.files ?? []); + input.remove(); + if (files2.length === 0) + return; + await this.plugin.ensureFolder(this.plugin.settings.dictionariesFolder); + let ok = 0; + for (const f of files2) { + try { + const text = await f.text(); + const parsed = JSON.parse(text); + if (!parsed.entries || !Array.isArray(parsed.entries)) + throw new Error("Format invalide"); + const dest = `${this.plugin.settings.dictionariesFolder}/${f.name}`; + const existing = this.app.vault.getAbstractFileByPath(dest); + if (existing instanceof import_obsidian7.TFile) { + await this.app.vault.modify(existing, text); + } else { + await this.app.vault.create(dest, text); + } + ok++; + } catch { + new import_obsidian7.Notice(`Format invalide : ${f.name}`); + } + } + if (ok > 0) + new import_obsidian7.Notice(`\u2713 ${ok} dictionnaire${ok > 1 ? "s" : ""} import\xE9${ok > 1 ? "s" : ""}`); + await this.renderTab("dictionaries"); + })(); + }); + input.click(); }); + const folder = this.app.vault.getAbstractFileByPath(this.plugin.settings.dictionariesFolder); + const files = []; + if (folder && "children" in folder) { + for (const child of folder.children) { + if (child instanceof import_obsidian7.TFile && child.name.endsWith(".json")) + files.push(child); + } + } + if (files.length === 0) { + el.createEl("p", { text: "Aucun dictionnaire import\xE9.", cls: "pseudobs-view-hint" }); + return; + } + el.createEl("p", { + text: `${files.length} dictionnaire${files.length > 1 ? "s" : ""} charg\xE9${files.length > 1 ? "s" : ""} :`, + cls: "pseudobs-view-count" + }); + const list = el.createEl("ul", { cls: "pseudobs-dict-list" }); + for (const f of files) { + let entryCount = "?"; + try { + const raw = await this.app.vault.read(f); + const parsed = JSON.parse(raw); + entryCount = String(parsed.entries?.length ?? 0); + } catch { + } + const li = list.createEl("li", { cls: "pseudobs-dict-item" }); + li.createSpan({ text: f.basename, cls: "pseudobs-dict-name" }); + li.createSpan({ text: `${entryCount} entr\xE9es`, cls: "pseudobs-dict-count" }); + const removeBtn = li.createEl("button", { text: "\u2715", cls: "pseudobs-dict-remove" }); + removeBtn.title = "Supprimer ce dictionnaire"; + removeBtn.addEventListener("click", () => { + void (async () => { + await this.app.fileManager.trashFile(f); + await this.renderTab("dictionaries"); + })(); + }); + } } // ---- Onglet Exports -------------------------------------------- renderExportsTab(el) { diff --git a/src/ui/PseudonymizationView.ts b/src/ui/PseudonymizationView.ts index ac2583a..5b3adbc 100644 --- a/src/ui/PseudonymizationView.ts +++ b/src/ui/PseudonymizationView.ts @@ -1,4 +1,5 @@ import { ItemView, Notice, Setting, TFile, WorkspaceLeaf } from 'obsidian'; +import type { DictionaryFile } from '../types'; import type PseudObsPlugin from '../main'; import type { MappingRule, Occurrence } from '../types'; import { scanOccurrences } from '../scanner/OccurrenceScanner'; @@ -124,7 +125,7 @@ export class PseudonymizationView extends ItemView { pane.empty(); if (tab === 'occurrences') await this.renderOccurrencesTab(pane); else if (tab === 'mappings') await this.renderMappingsTab(pane); - else if (tab === 'dictionaries') this.renderDictionariesTab(pane); + else if (tab === 'dictionaries') await this.renderDictionariesTab(pane); else if (tab === 'ner') await this.renderNerTab(pane); else this.renderExportsTab(pane); } @@ -457,15 +458,91 @@ export class PseudonymizationView extends ItemView { // ---- Onglet Dictionnaires -------------------------------------- - private renderDictionariesTab(el: HTMLElement): void { - el.createEl('p', { - text: 'La gestion avancée des dictionnaires (détection NER, listes de lieux et d\'institutions) sera disponible en Phase 9.', - cls: 'pseudobs-view-hint', + private async renderDictionariesTab(el: HTMLElement): Promise { + const toolbar = el.createDiv('pseudobs-view-toolbar'); + const importBtn = toolbar.createEl('button', { + text: 'Importer un dictionnaire (.dict.json)', + cls: 'pseudobs-view-add-btn', }); - el.createEl('p', { - text: 'Utilisez l\'outil Coulmont via le menu contextuel (clic droit sur un prénom dans la transcription).', - cls: 'pseudobs-view-hint', + const statusEl = toolbar.createSpan({ cls: 'pseudobs-view-dict-status' }); + + importBtn.addEventListener('click', () => { + const input = activeDocument.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.multiple = true; + input.classList.add('pseudobs-hidden-input'); + activeDocument.body.appendChild(input); + input.addEventListener('change', () => { void (async () => { + const files = Array.from(input.files ?? []); + input.remove(); + if (files.length === 0) return; + + await this.plugin.ensureFolder(this.plugin.settings.dictionariesFolder); + let ok = 0; + for (const f of files) { + try { + const text = await f.text(); + const parsed = JSON.parse(text) as DictionaryFile; + if (!parsed.entries || !Array.isArray(parsed.entries)) throw new Error('Format invalide'); + const dest = `${this.plugin.settings.dictionariesFolder}/${f.name}`; + const existing = this.app.vault.getAbstractFileByPath(dest); + if (existing instanceof TFile) { + await this.app.vault.modify(existing, text); + } else { + await this.app.vault.create(dest, text); + } + ok++; + } catch { + new Notice(`Format invalide : ${f.name}`); + } + } + if (ok > 0) new Notice(`✓ ${ok} dictionnaire${ok > 1 ? 's' : ''} importé${ok > 1 ? 's' : ''}`); + await this.renderTab('dictionaries'); + })(); }); + input.click(); }); + + // Liste des dictionnaires présents dans le vault + const folder = this.app.vault.getAbstractFileByPath(this.plugin.settings.dictionariesFolder); + const files: TFile[] = []; + if (folder && 'children' in folder) { + for (const child of (folder as { children: unknown[] }).children) { + if (child instanceof TFile && child.name.endsWith('.json')) files.push(child); + } + } + + if (files.length === 0) { + el.createEl('p', { text: 'Aucun dictionnaire importé.', cls: 'pseudobs-view-hint' }); + return; + } + + el.createEl('p', { + text: `${files.length} dictionnaire${files.length > 1 ? 's' : ''} chargé${files.length > 1 ? 's' : ''} :`, + cls: 'pseudobs-view-count', + }); + + const list = el.createEl('ul', { cls: 'pseudobs-dict-list' }); + for (const f of files) { + let entryCount = '?'; + try { + const raw = await this.app.vault.read(f); + const parsed = JSON.parse(raw) as DictionaryFile; + entryCount = String(parsed.entries?.length ?? 0); + } catch { /* fichier malformé */ } + + const li = list.createEl('li', { cls: 'pseudobs-dict-item' }); + li.createSpan({ text: f.basename, cls: 'pseudobs-dict-name' }); + li.createSpan({ text: `${entryCount} entrées`, cls: 'pseudobs-dict-count' }); + const removeBtn = li.createEl('button', { text: '✕', cls: 'pseudobs-dict-remove' }); + removeBtn.title = 'Supprimer ce dictionnaire'; + removeBtn.addEventListener('click', () => { void (async () => { + await this.app.fileManager.trashFile(f); + await this.renderTab('dictionaries'); + })(); }); + } + + void statusEl; } // ---- Onglet Exports -------------------------------------------- diff --git a/styles.css b/styles.css index 57376ab..d384a4e 100644 --- a/styles.css +++ b/styles.css @@ -580,3 +580,22 @@ .pseudobs-view-add-btn::before { content: '+ '; } .pseudobs-btn-saved::before { content: '✓ '; color: var(--color-green); } .pseudobs-ner-reset-btn { margin-left: 8px; } + +/* Onglet Dictionnaires — panneau latéral */ +.pseudobs-dict-list { list-style: none; padding: 0; margin: 6px 0; } +.pseudobs-dict-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 6px; + border-radius: 4px; + font-size: 0.85em; +} +.pseudobs-dict-item:hover { background: var(--background-secondary); } +.pseudobs-dict-name { flex: 1; font-family: var(--font-monospace); } +.pseudobs-dict-count { color: var(--text-muted); font-size: 0.8em; white-space: nowrap; } +.pseudobs-dict-remove { + background: none; border: none; cursor: pointer; + color: var(--text-faint); padding: 0 4px; +} +.pseudobs-dict-remove:hover { color: var(--color-red); }