From 34d89706bef6b6aa85ddf5118c0db1095c7f8b6e Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 2 Jun 2026 15:08:30 -0400 Subject: [PATCH] =?UTF-8?q?feat:=206=20nouvelles=20commandes=20palette=20?= =?UTF-8?q?=E2=80=94=20cr=C3=A9er/supprimer=20fichiers/dossiers,=20=C3=A9d?= =?UTF-8?q?iter,=20pop-out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/js/palette.js | 208 +++++++++++++++++++++++++++++++++++------ 1 file changed, 177 insertions(+), 31 deletions(-) diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 1b91416..ffe00a1 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -1,10 +1,11 @@ -/* ObsiGate — Command Palette (Ctrl+P) +/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) Quick file opener and command runner. - Inspired by VS Code's Ctrl+P / Ctrl+Shift+P. + Inspired by VS Code's command palette. */ import { api } from './auth.js'; import { openFile } from './viewer.js'; -import { showToast, toggleTheme } from './ui.js'; +import { showToast, toggleTheme, TabManager } from './ui.js'; +import { openEditor } from './utils.js'; import { state } from './state.js'; // ── Helper: trigger goHome by clicking the logo ── @@ -13,24 +14,187 @@ function triggerGoHome() { if (logo) logo.click(); } -// ── DOM cache (created in open) ── +// ── Get current active file from tabs ── +function getCurrentFile() { + const activeId = TabManager._activeTabId; + if (activeId) { + const tab = TabManager._tabs.find(t => t.id === activeId); + if (tab) return { vault: tab.vault, path: tab.path }; + } + // Fallback to state + if (state.currentVault && state.currentPath) { + return { vault: state.currentVault, path: state.currentPath }; + } + return null; +} + +// ── Get current vault from tabs ── +function getCurrentVault() { + const file = getCurrentFile(); + if (file) return file.vault; + if (state.currentVault) return state.currentVault; + if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name; + return null; +} + +// ── Prompt for vault if ambiguous ── +function promptVault(hint) { + const vault = getCurrentVault(); + if (vault) return vault; + return prompt(`${hint} — Nom de la vault :`) || ''; +} + +// ── DOM cache ── let _overlay = null; let _input = null; let _results = null; -let _mode = 'files'; // 'files' | 'commands' +let _mode = 'files'; let _selectedIndex = 0; let _searchTimeout = null; let _lastResults = []; // ── Available commands ── const COMMANDS = [ - { id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', action: () => { close(); triggerGoHome(); } }, - { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer clair/sombre', action: () => { close(); toggleTheme(); } }, - { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète', action: async () => { close(); try { await api('/api/index/reload'); showToast('Index rechargé', 'success'); } catch { showToast('Erreur de réindexation', 'error'); } } }, - { id: 'help', label: '❓ Aide', description: 'Ouvrir l\'aide', action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } }, - { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } }, + // ── Navigation ── + { id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', category: 'Navigation', + action: () => { close(); triggerGoHome(); } }, + + // ── Fichiers ── + { id: 'edit-current', label: '✏️ Éditer fichier courant', description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', + action: () => { close(); const f = getCurrentFile(); if (f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert', 'error'); } }, + + { id: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document courant dans une nouvelle fenêtre', category: 'Fichiers', + action: () => { close(); const f = getCurrentFile(); if (f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`, `popout_${f.vault}_${f.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); else showToast('Aucun fichier ouvert', 'error'); } }, + + { id: 'create-file', label: '📄 Créer fichier markdown', description: 'Créer un nouveau fichier .md et l\'ouvrir dans l\'éditeur', category: 'Fichiers', + action: async () => { close(); await createFile(); } }, + + { id: 'delete-current', label: '🗑️ Supprimer fichier courant', description: 'Supprimer le fichier ouvert dans l\'onglet actif', category: 'Fichiers', + action: async () => { close(); await deleteCurrentFile(); } }, + + // ── Répertoires ── + { id: 'create-dir', label: '📁 Créer répertoire', description: 'Créer un nouveau dossier', category: 'Répertoires', + action: async () => { close(); await createDirectory(); } }, + + { id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Supprimer un dossier et son contenu', category: 'Répertoires', + action: async () => { close(); await deleteDirectory(); } }, + + // ── Actions ── + { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer entre thème clair et sombre', category: 'Actions', + action: () => { close(); toggleTheme(); } }, + + { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète des vaults', category: 'Actions', + action: async () => { close(); try { await api('/api/index/reload'); showToast('Index rechargé', 'success'); } catch { showToast('Erreur de réindexation', 'error'); } } }, + + // ── Aide & Config ── + { id: 'help', label: '❓ Aide', description: 'Ouvrir le guide d\'utilisation', category: 'Système', + action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } }, + + { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', + action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } }, ]; +// ── Create file action ── +async function createFile() { + const vault = promptVault('Créer un fichier'); + if (!vault) return; + + let path = prompt('Chemin du fichier (ex: notes/mon-fichier) :'); + if (!path) return; + + // Add .md extension if none + if (!path.includes('.')) path += '.md'; + + try { + const data = await api(`/api/file/${encodeURIComponent(vault)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, content: `# ${path.split('/').pop().replace('.md', '')}\n\n` }), + }); + if (data?.status === 'ok' || data?.success) { + showToast(`✅ ${path} créé`, 'success'); + openEditor(vault, path); + } else { + showToast('Erreur de création', 'error'); + } + } catch (e) { + showToast(`Erreur : ${e.message || e}`, 'error'); + } +} + +// ── Create directory action ── +async function createDirectory() { + const vault = promptVault('Créer un dossier'); + if (!vault) return; + + const path = prompt('Chemin du dossier (ex: notes/projets) :'); + if (!path) return; + + try { + const data = await api(`/api/directory/${encodeURIComponent(vault)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + if (data?.status === 'ok' || data?.success) { + showToast(`📁 ${path} créé`, 'success'); + } else { + showToast('Erreur de création', 'error'); + } + } catch (e) { + showToast(`Erreur : ${e.message || e}`, 'error'); + } +} + +// ── Delete current file action ── +async function deleteCurrentFile() { + const f = getCurrentFile(); + if (!f) { showToast('Aucun fichier ouvert', 'error'); return; } + + if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return; + + try { + const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { + method: 'DELETE', + }); + if (data?.status === 'ok' || data?.success) { + showToast(`🗑️ ${f.path} supprimé`, 'success'); + // Close the tab + const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path); + if (tab) TabManager.close(tab.id); + triggerGoHome(); + } else { + showToast('Erreur de suppression', 'error'); + } + } catch (e) { + showToast(`Erreur : ${e.message || e}`, 'error'); + } +} + +// ── Delete directory action ── +async function deleteDirectory() { + const vault = promptVault('Supprimer un dossier'); + if (!vault) return; + + const path = prompt('Chemin du dossier à supprimer (ex: notes/obsolète) :'); + if (!path) return; + + if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return; + + try { + const data = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { + method: 'DELETE', + }); + if (data?.status === 'ok' || data?.success) { + showToast(`🗑️ ${path} supprimé`, 'success'); + } else { + showToast('Erreur de suppression', 'error'); + } + } catch (e) { + showToast(`Erreur : ${e.message || e}`, 'error'); + } +} + // ── Create DOM structure ── function createDOM() { if (_overlay) return; @@ -56,19 +220,16 @@ function createDOM() { _input = _overlay.querySelector('.cp-input'); _results = _overlay.querySelector('.cp-results'); - // Click outside to close (on overlay, not the palette dialog) _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); }); - // Keyboard events _input.addEventListener('keydown', onKeyDown); _input.addEventListener('input', onInput); document.body.appendChild(_overlay); } -// ── Open / Close ── export function open(initialQuery = '') { createDOM(); _overlay.classList.add('active'); @@ -78,7 +239,6 @@ export function open(initialQuery = '') { _lastResults = []; _results.innerHTML = ''; _input.focus(); - // Trigger initial search onInput(); } @@ -87,7 +247,6 @@ export function close() { _overlay.classList.remove('active'); if (_searchTimeout) clearTimeout(_searchTimeout); _searchTimeout = null; - // Restore focus const searchInput = document.querySelector('.search-input'); if (searchInput) searchInput.focus(); } @@ -96,11 +255,8 @@ function isOpen() { return _overlay && _overlay.classList.contains('active'); } -// ── Input handler ── function onInput() { const val = _input.value; - - // Detect mode change const newMode = val.startsWith('>') ? 'commands' : 'files'; if (newMode !== _mode) { _mode = newMode; @@ -120,7 +276,6 @@ function onInput() { }, 150); } -// ── Search files (via /api/suggest) ── async function searchFiles(query) { if (!query) { _results.innerHTML = '
Commencez à taper pour chercher un fichier
'; @@ -145,7 +300,6 @@ async function searchFiles(query) { } } -// ── Search commands ── function searchCommands(query) { if (!query) { _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); @@ -155,12 +309,11 @@ function searchCommands(query) { const lower = query.toLowerCase(); _lastResults = COMMANDS - .filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower)) + .filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower) || (c.category && c.category.toLowerCase().includes(lower))) .map(c => ({ type: 'command', ...c })); renderResults(); } -// ── Render results list ── function renderResults() { _selectedIndex = 0; @@ -176,7 +329,6 @@ function renderResults() { `).join(''); - // Click handler _results.querySelectorAll('.cp-item').forEach(el => { el.addEventListener('click', () => { const idx = parseInt(el.dataset.index); @@ -192,7 +344,6 @@ function renderResults() { scrollToSelected(); } -// ── Keyboard navigation ── function onKeyDown(e) { const items = _results.querySelectorAll('.cp-item'); @@ -212,10 +363,8 @@ function onKeyDown(e) { updateSelection(items); } else if (e.key === 'Tab') { e.preventDefault(); - // Switch between files/commands mode if (_mode === 'files') { _input.value = '> '; - // Move cursor to end setTimeout(() => { _input.selectionStart = _input.selectionEnd = _input.value.length; }, 0); @@ -240,7 +389,6 @@ function scrollToSelected() { } } -// ── Execute ── function executeItem(index) { const item = _lastResults[index]; if (!item) return; @@ -253,7 +401,6 @@ function executeItem(index) { } } -// ── HTML escaping ── function escapeHtml(str) { if (!str) return ''; return String(str) @@ -263,15 +410,14 @@ function escapeHtml(str) { .replace(/"/g, '"'); } -// ── Init: Register global keyboard shortcut ── export function initCommandPalette() { document.addEventListener('keydown', (e) => { - // Ctrl+Shift+Espace — open file palette + // Ctrl+Shift+Espace — file palette if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) { close(); } else { open(''); } } - // Ctrl+Alt+Espace — open command palette + // Ctrl+Alt+Espace — command palette if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) { close(); } else { open('> '); }