diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 730ad8e..487b81d 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -1,273 +1,205 @@ -/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) - Quick file opener and command runner. - Inspired by VS Code's command palette. -*/ +/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */ import { api } from './auth.js'; import { openFile } from './viewer.js'; import { showToast, toggleTheme, TabManager } from './ui.js'; import { openEditor } from './utils.js'; import { state } from './state.js'; -// ── Helper: trigger goHome ── -function triggerGoHome() { - const el = document.getElementById('header-logo'); - if (el) el.click(); -} +function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); } -// ── Get current active file ── function getCurrentFile() { const id = TabManager._activeTabId; - if (id) { - const tab = TabManager._tabs.find(t => t.id === id); - if (tab) return { vault: tab.vault, path: tab.path }; - } + if (id) { const t = TabManager._tabs.find(x => x.id === id); if (t) return { vault: t.vault, path: t.path }; } if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath }; return null; } -function getCurrentVault() { - const f = getCurrentFile(); - if (f) return f.vault; - if (state.currentVault) return state.currentVault; - if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name; - return null; -} - // ── DOM / state ── -let _overlay = null; -let _input = null; -let _results = null; -let _mode = 'files'; // 'files' | 'commands' | 'browse' -let _selectedIndex = 0; -let _searchTimeout = null; -let _lastResults = []; - -// Browse mode state -let _browse = { - action: null, // 'create-file' | 'create-dir' | 'delete-dir' - vault: null, - path: '', - name: '', -}; +let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = []; +let _browse = { action: null, vault: null, path: '' }; // ── Commands ── -const COMMANDS = [ - { id: 'home', label: '🏠 Accueil', description: "Retour à l'accueil", category: 'Navigation', action: () => { close(); triggerGoHome(); } }, - { 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 dans une fenêtre détachée', category: 'Fichiers', action: () => { close(); const f=getCurrentFile(); if(f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700'); else showToast('Aucun fichier ouvert','error'); } }, - { id: 'create-file', label: '📄 Créer fichier markdown',description: 'Naviguer vers le dossier parent, puis nommer le fichier', category: 'Fichiers', action: () => { startBrowse('create-file'); } }, - { id: 'delete-current',label: '🗑️ Supprimer fichier courant',description: 'Supprimer le fichier de l\'onglet actif', category: 'Fichiers', action: async () => { close(); await deleteCurrentFile(); } }, - { id: 'create-dir', label: '📁 Créer répertoire', description: 'Naviguer vers le dossier parent, puis nommer le dossier', category: 'Répertoires', action: () => { startBrowse('create-dir'); } }, - { id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Naviguer et sélectionner le dossier à supprimer', category: 'Répertoires', action: () => { startBrowse('delete-dir'); } }, - { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer clair/sombre', category: 'Actions', action: () => { close(); toggleTheme(); } }, - { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète', category: 'Actions', 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 le guide d'utilisation", category: 'Système', action: () => { close(); const b=document.getElementById('help-open-btn'); if(b)b.click(); } }, - { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', action: () => { close(); const b=document.getElementById('config-open-btn'); if(b)b.click(); } }, +const CMDS = [ + { id:'home', label:'🏠 Accueil', desc:"Retour à l'accueil", cat:'Navigation', act:()=>{close();triggerGoHome();} }, + { id:'edit-current', label:'✏️ Éditer fichier courant', desc:'Ouvrir le fichier actif dans l\'éditeur', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast('Aucun fichier ouvert','error');} }, + { id:'popout', label:'🪟 Pop-out', desc:'Ouvrir le document dans une fenêtre', cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700');else showToast('Aucun fichier ouvert','error');} }, + { id:'create-file', label:'📄 Créer fichier markdown', desc:'Choisir la vault, naviguer, puis nommer le fichier', cat:'Fichiers', act:()=>startBrowse('create-file') }, + { id:'delete-cur', label:'🗑️ Supprimer fichier courant', desc:'Supprimer le fichier de l\'onglet actif', cat:'Fichiers', act:async()=>{close();await delCurFile();} }, + { id:'create-dir', label:'📁 Créer répertoire', desc:'Choisir la vault, naviguer, puis nommer le dossier', cat:'Répertoires', act:()=>startBrowse('create-dir') }, + { id:'delete-dir', label:'🗑️ Supprimer répertoire', desc:'Choisir la vault, naviguer et supprimer', cat:'Répertoires', act:()=>startBrowse('delete-dir') }, + { id:'theme', label:'🌓 Changer le thème', desc:'Basculer clair/sombre', cat:'Actions', act:()=>{close();toggleTheme();} }, + { id:'reindex', label:'🔄 Réindexer', desc:'Forcer la réindexation complète', cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast('Index rechargé','success');}catch{showToast('Erreur','error');}} }, + { id:'help', label:'❓ Aide', desc:'Ouvrir le guide', cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} }, + { id:'config', label:'⚙️ Configuration', desc:'Ouvrir les paramètres', cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} }, ]; -// ── Browse mode helpers ── +// ── Browse: vault picker ── function startBrowse(action) { - const vault = getCurrentVault(); - if (!vault) { showToast('Aucune vault trouvée', 'error'); return; } - _browse = { action, vault, path: '', name: '' }; - switchMode('browse'); - _input.value = ''; - renderBrowse(); + _browse = { action, vault: null, path: '' }; + switchMode('browse-vault'); + renderVaultPicker(); } -function switchMode(newMode) { - _mode = newMode; - if (_mode === 'browse') { - _input.placeholder = 'Naviguez avec ↑↓, Enter pour entrer, Retour arrière pour monter'; - _input.disabled = true; - _input.style.display = 'none'; - } else { - _input.disabled = false; - _input.style.display = ''; - const bc = document.getElementById('cp-breadcrumb'); - if (bc) bc.style.display = 'none'; - } -} - -// ── Render directory browser ── -async function renderBrowse() { - _selectedIndex = 0; - _lastResults = []; - const { vault, path } = _browse; - const breadcrumbEl = document.getElementById('cp-breadcrumb'); - - // Show breadcrumb - if (breadcrumbEl) { - breadcrumbEl.style.display = 'flex'; - const segments = path ? path.split('/') : []; - let html = `${escapeHtml(vault)}`; - let cumul = ''; - for (const seg of segments) { - cumul = cumul ? cumul + '/' + seg : seg; - html += `/${escapeHtml(seg)}`; +async function renderVaultPicker() { + try { + const data = await api('/api/vaults'); + const vaults = Array.isArray(data) ? data : []; + _items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: v.file_count+' fichiers' })); + if (_items.length === 0) { + _items = [{ type:'_empty', label: 'Aucune vault trouvée', sublabel: '' }]; } - breadcrumbEl.innerHTML = html; + render(); + } catch { _items = [{ type:'_error', label: 'Erreur de chargement', sublabel: '' }]; render(); } +} - // Click on breadcrumb segment to jump - breadcrumbEl.querySelectorAll('.cp-bc-item[data-path]').forEach(el => { - el.addEventListener('click', () => { - _browse.path = el.dataset.path; - renderBrowse(); - }); - }); +// ── Browse: directory navigation ── +async function renderBrowse() { + const { vault, path } = _browse; + _items = []; + const bc = document.getElementById('cp-breadcrumb'); + if (bc) { + bc.style.display = 'flex'; + const segs = path ? path.split('/') : []; + let html = `${esc(vault)}`; + let acc = ''; + for (const s of segs) { acc = acc ? acc+'/'+s : s; html += `/${esc(s)}`; } + bc.innerHTML = html; + bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; renderBrowse(); })); + bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; renderBrowse(); }); } try { const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`); const items = data.items || []; - const dirs = items.filter(i => i.type === 'directory'); - const files = items.filter(i => i.type === 'file'); + const dirs = items.filter(i => i.type === 'directory' || i.is_dir); + const files = items.filter(i => i.type === 'file' && !i.is_dir); - // Build result list - const segments = path ? path.split('/') : []; - const breadcrumb = vault + (path ? '/' + path : ''); - - // ── Action row (first result) ── + // Action row + const actLabel = { 'create-file':'📄 Créer fichier ici', 'create-dir':'📁 Créer dossier ici', 'delete-dir':'🗑️ Supprimer ce dossier' }; if (_browse.action === 'delete-dir' && path) { - _lastResults.push({ - type: '_action', id: 'confirm-delete', - label: '🗑️ ' + (path.split('/').pop() || path), - sublabel: 'Supprimer ce dossier et tout son contenu (confirmation demandée)', - action: () => executeBrowseDelete(path), - }); - } else if (_browse.action === 'create-file' || _browse.action === 'create-dir') { - const actionLabel = _browse.action === 'create-file' ? '📄 Créer un fichier ici' : '📁 Créer un dossier ici'; - _lastResults.push({ - type: '_action', id: 'confirm-create', - label: actionLabel, - sublabel: 'Dans ' + (breadcrumb || 'racine de la vault'), - action: () => executeBrowseCreate(), - }); + _items.push({ type:'_action', id:'confirm', label: '🗑️ Supprimer '+ (path.split('/').pop()||path), sublabel: 'Tout le contenu sera supprimé', act:()=>execDelDir(path) }); + } else if (_browse.action !== 'delete-dir') { + _items.push({ type:'_action', id:'confirm', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() }); } - // ── Parent entry ── + // Parent if (path) { - const parent = segments.slice(0, -1).join('/'); - _lastResults.push({ - type: '_navigate', id: 'parent', - label: '📂 .. (dossier parent)', - sublabel: '', - go: parent, - }); + const parent = path.split('/').slice(0,-1).join('/'); + _items.push({ type:'_nav', label: '📂 .. (parent)', go: parent }); } - // ── Subdirectories ── + // Subdirs for (const d of dirs) { - const full = path ? path + '/' + d.name : d.name; - _lastResults.push({ - type: '_navigate', id: 'dir-' + full, - label: '📁 ' + d.name, - sublabel: '', - go: full, - }); + const fp = path ? path+'/'+d.name : d.name; + _items.push({ type:'_nav', label: '📁 '+d.name, go: fp }); } + // Files (info) + for (const f of files) _items.push({ type:'_info', label: '📄 '+f.name, sublabel: '' }); - // ── Files (for info, not clickable for creating) ── - for (const f of files) { - _lastResults.push({ - type: '_file', - label: '📄 ' + f.name, - sublabel: '', - }); - } - - if (_lastResults.length === 0) { - _lastResults.push({ type: '_empty', label: '📂 Dossier vide', sublabel: '' }); - } - } catch { - _lastResults = [{ type: '_error', label: 'Erreur de chargement', sublabel: '' }]; - } - - renderResults(); + if (_items.length === 0) _items.push({ type:'_empty', label: '📂 Dossier vide', sublabel: '' }); + } catch { _items = [{ type:'_error', label: 'Erreur', sublabel: '' }]; } + render(); } -async function executeBrowseDelete(path) { - if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return; +// ── Name input modal ── +function askName() { + const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier'; + const ext = _browse.action === 'create-file' ? '(.md ajouté si besoin)' : ''; + const existingInput = document.getElementById('cp-name-input'); + if (existingInput) { existingInput.focus(); return; } + + _input.style.display = 'none'; + const bc = document.getElementById('cp-breadcrumb'); + if (bc) bc.style.display = 'none'; + + const nameDiv = document.createElement('div'); + nameDiv.id = 'cp-name-input'; + nameDiv.className = 'cp-name-input'; + nameDiv.innerHTML = ` + Enter confirmer · Esc annuler`; + _results.before(nameDiv); + + const field = document.getElementById('cp-name-field'); + field.focus(); + field.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const name = field.value.trim(); + if (!name) return; + cleanupNameInput(); + if (_browse.action === 'create-file') execCreateFile(name); + else execCreateDir(name); + } else if (e.key === 'Escape') { + e.preventDefault(); + cleanupNameInput(); + renderBrowse(); + } + }); +} + +function cleanupNameInput() { + const el = document.getElementById('cp-name-input'); + if (el) el.remove(); + _input.style.display = ''; + const bc = document.getElementById('cp-breadcrumb'); + if (bc) bc.style.display = 'flex'; +} + +// ── Execute actions ── +async function execCreateFile(name) { + let fp = _browse.path ? _browse.path+'/'+name : name; + if (!fp.includes('.')) fp += '.md'; try { - const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method: 'DELETE' }); - if (data?.status === 'ok' || data?.success) { - showToast(`🗑️ ${path} supprimé`, 'success'); - close(); - } else showToast('Erreur de suppression', 'error'); - } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } + const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:fp, content:`# ${name.replace('.md','')}\n\n`}) }); + if (d?.status==='ok' || d?.success) { showToast(`✅ ${fp} créé`,'success'); close(); openEditor(_browse.vault, fp); } + else showToast('Erreur de création','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } -async function executeBrowseCreate() { - const action = _browse.action; - const label = action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier'; - const name = prompt(label + ' dans ' + (_browse.path || 'racine') + ' :'); - if (!name) return; - if (action === 'create-file') { - let filePath = _browse.path ? _browse.path + '/' + name : name; - if (!filePath.includes('.')) filePath += '.md'; - try { - const data = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: filePath, content: `# ${name.replace('.md','')}\n\n` }), - }); - if (data?.status === 'ok' || data?.success) { - showToast(`✅ ${filePath} créé`, 'success'); - close(); - openEditor(_browse.vault, filePath); - } else showToast('Erreur de création', 'error'); - } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } - } else { - const dirPath = _browse.path ? _browse.path + '/' + name : name; - try { - const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: dirPath }), - }); - if (data?.status === 'ok' || data?.success) { - showToast(`📁 ${dirPath} créé`, 'success'); - close(); - } else showToast('Erreur de création', 'error'); - } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } - } +async function execCreateDir(name) { + const dp = _browse.path ? _browse.path+'/'+name : name; + try { + const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:dp}) }); + if (d?.status==='ok' || d?.success) { showToast(`📁 ${dp} créé`,'success'); close(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } -// ── Delete current file ── -async function deleteCurrentFile() { +async function execDelDir(path) { + if (!confirm(`Supprimer "${path}" et TOUT son contenu ?`)) return; + try { + const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); + if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } +} + +async function delCurFile() { const f = getCurrentFile(); - if (!f) { showToast('Aucun fichier ouvert', 'error'); return; } - if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return; + if (!f) { showToast('Aucun fichier ouvert','error'); return; } + if (!confirm(`Supprimer "${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'); - 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'); } + const d = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method:'DELETE' }); + if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${f.path} supprimé`,'success'); + const tab = TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path); if(tab) TabManager.close(tab.id); triggerGoHome(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } -// ── Create DOM ── +function refreshSidebar() { + // Try to refresh the vault tree + import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {}); +} + +// ── DOM ── function createDOM() { if (_overlay) return; _overlay = document.createElement('div'); _overlay.className = 'command-palette-overlay'; - _overlay.innerHTML = ` -