/** * Vault Home Page module for ObsiGate. * * When a specific vault is selected (not "All"), replaces the dashboard * with a recursive file listing sorted by modification time (newest first). * Files from the selected directory AND all its subdirectories are shown * with their relative path displayed in the metadata line. * The listing updates automatically when browsing into subdirectories. * * The visual style mirrors the search results (.search-result-item) for * consistency. * * Usage: * import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js'; */ import { api } from './auth.js'; import { el } from './viewer.js'; import { getFileIcon, safeCreateIcons } from './utils.js'; import { TabManager } from './ui.js'; // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- const state = { active: false, currentVault: null, currentDir: '', // empty = vault root }; // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** Check if vault home is currently displayed. */ export function isVaultHomeActive() { return state.active; } /** * Display the vault home page for a vault directory. * @param {string} vaultName - Vault name * @param {string} dirPath - Directory path (empty for root) */ export async function showVaultHome(vaultName, dirPath = '') { if (vaultName === 'all' || !vaultName) { state.active = false; return; } state.active = true; state.currentVault = vaultName; state.currentDir = dirPath || ''; const area = document.getElementById('content-area'); if (!area) return; // Remove any existing dashboard const home = document.getElementById('dashboard-home'); if (home) home.remove(); // Build vault home container area.innerHTML = ''; const container = el('div', { class: 'vault-home', id: 'vault-home' }); area.appendChild(container); // Breadcrumb renderBreadcrumb(container, vaultName, state.currentDir); // Results container const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' }); container.appendChild(resultsArea); // Show loading resultsArea.innerHTML = `

Chargement des fichiers...

`; await loadAndRender(vaultName, state.currentDir, resultsArea); // Also load recent files for this vault (only at root level) if (!state.currentDir) { loadRecentSection(vaultName, container); } } /** * Update the vault home directory without rebuilding the whole page. * Called when navigating into a subdirectory. * @param {string} vaultName * @param {string} dirPath */ export async function updateVaultHomeDir(vaultName, dirPath) { if (!state.active || state.currentVault !== vaultName) return; state.currentDir = dirPath || ''; const container = document.getElementById('vault-home'); if (!container) return; // Update breadcrumb const bc = container.querySelector('.vault-home-breadcrumb'); if (bc) { bc.innerHTML = ''; buildBreadcrumbHTML(bc, vaultName, state.currentDir); safeCreateIcons(); } // Reload results const resultsArea = document.getElementById('vault-home-results'); if (resultsArea) { resultsArea.innerHTML = `
`; await loadAndRender(vaultName, state.currentDir, resultsArea); } } // --------------------------------------------------------------------------- // Tree click delegation — update vault home on directory clicks // --------------------------------------------------------------------------- function _initTreeDelegation() { const tree = document.getElementById('vault-tree'); if (!tree) return; tree.addEventListener('click', (e) => { if (!state.active) return; // Find the closest tree-item that is a directory const item = e.target.closest('.tree-item'); if (!item || !item.dataset.vault || item.dataset.path === undefined) return; const vault = item.dataset.vault; const path = item.dataset.path; // Only react to directory clicks (items that have a child container) const hasChildContainer = item.nextElementSibling && item.nextElementSibling.classList.contains('tree-children'); if (!hasChildContainer) return; // Update vault home dir updateVaultHomeDir(vault, path); }); } // Initialize delegation when module loads _initTreeDelegation(); // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- function renderBreadcrumb(container, vaultName, dirPath) { const bc = el('div', { class: 'vault-home-breadcrumb' }); buildBreadcrumbHTML(bc, vaultName, dirPath); container.appendChild(bc); } function buildBreadcrumbHTML(parentEl, vaultName, dirPath) { // Vault icon + name (root) const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [ el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }), el('span', {}, [document.createTextNode(vaultName)]), ]); vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, '')); vaultLink.style.cursor = 'pointer'; parentEl.appendChild(vaultLink); if (!dirPath) return; const segments = dirPath.split('/').filter(Boolean); let cumulative = ''; for (const seg of segments) { cumulative = cumulative ? cumulative + '/' + seg : seg; const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]); parentEl.appendChild(sep); const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [ el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }), el('span', {}, [document.createTextNode(seg)]), ]); segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative)); segLink.style.cursor = 'pointer'; parentEl.appendChild(segLink); } } async function loadAndRender(vaultName, dirPath, container) { try { const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=true`; const data = await api(url); if (!data.files || data.files.length === 0) { container.innerHTML = `

Aucun fichier dans ce répertoire

`; safeCreateIcons(); return; } // Header const header = el('div', { class: 'vault-home-header' }); const count = el('span', { class: 'vault-home-count' }, [ document.createTextNode(`${data.files.length} fichier${data.files.length > 1 ? 's' : ''}`), ]); header.appendChild(count); // Recursive indicator if (data.recursive) { const recursiveBadge = el('span', { class: 'vault-home-recursive-badge' }, [ document.createTextNode('récursif'), ]); header.appendChild(recursiveBadge); } // Directory label const dirLabel = dirPath ? `/${dirPath}` : ' (racine)'; const dirInfo = el('span', { class: 'vault-home-dir-info' }, [ document.createTextNode(dirLabel), ]); header.appendChild(dirInfo); container.innerHTML = ''; container.appendChild(header); // File list — using search-result-item style const list = el('div', { class: 'search-results' }); data.files.forEach((f) => { const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file'; const displayName = f.name.match(/\.md$/i) ? f.name.replace(/\.md$/i, '') : f.name; const ext = f.extension || ''; const titleDiv = el('div', { class: 'search-result-title' }, [ document.createTextNode(displayName), ]); // Format date const modDate = new Date(f.modified * 1000); const dateStr = modDate.toLocaleDateString('fr-CA', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); // Build metadata line: rel_dir (if any) + date + size const metaParts = []; if (f.rel_dir) { metaParts.push(f.rel_dir + '/'); } metaParts.push(dateStr); metaParts.push(formatSize(f.size)); const metaDiv = el('div', { class: 'search-result-vault' }, [ document.createTextNode(metaParts.join(' · ')), ]); const item = el('div', { class: 'search-result-item vault-home-item', 'data-vault': f.vault, 'data-path': f.path, }, [ el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]), el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }), titleDiv, metaDiv, ]); item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path)); item.addEventListener('dblclick', (e) => { e.preventDefault(); TabManager.openPersistent(f.vault, f.path); }); list.appendChild(item); }); container.appendChild(list); safeCreateIcons(); } catch (err) { container.innerHTML = `

Erreur de chargement: ${escapeHtml(err?.detail || err?.message || 'Inconnue')}

`; } } // --------------------------------------------------------------------------- // Recent files section (loaded at vault root only) // --------------------------------------------------------------------------- async function loadRecentSection(vaultName, container) { try { const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`); const files = data.files || []; if (files.length === 0) return; // Insert recent section BEFORE the results area const resultsArea = document.getElementById('vault-home-results'); const section = el('div', { class: 'vault-home-recent' }); const header = el('div', { class: 'vault-home-recent-header' }); header.innerHTML = ` Récents`; section.appendChild(header); const list = el('div', { class: 'vault-home-recent-list' }); files.forEach((f) => { const title = f.title || f.path.split('/').pop() || ''; const displayTitle = title.match(/\.md$/i) ? title.replace(/\.md$/i, '') : title; const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : ''; const item = el('div', { class: 'vault-home-recent-item' }); item.innerHTML = ` ${escapeHtml(displayTitle)} ${escapeHtml(dir)} ${escapeHtml(f.mtime_human || '')}`; item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path)); item.addEventListener('dblclick', (e) => { e.preventDefault(); TabManager.openPersistent(f.vault, f.path); }); list.appendChild(item); }); section.appendChild(list); // Insert before the results area if (resultsArea) { container.insertBefore(section, resultsArea); } else { container.appendChild(section); } safeCreateIcons(); } catch { // Silently skip if recent files can't be loaded } } // --------------------------------------------------------------------------- // Utility // --------------------------------------------------------------------------- function formatSize(bytes) { if (bytes < 1024) return bytes + ' o'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko'; return (bytes / (1024 * 1024)).toFixed(1) + ' Mo'; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }