/** * Vault Home Page module for ObsiGate. * * When a specific vault is selected (not "All"), replaces the dashboard * with a non-recursive file listing sorted by modification time (newest first). * Only files from the selected directory are shown (not subdirectories) * to keep the listing fast and lightweight. * 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'; import { t, getLocale } from './i18n.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 = `

${t('vaulthome.loading')}

`; 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(); } // Remove recent section when navigating into subdirectory const recentSection = container.querySelector('.vault-home-recent'); if (recentSection) recentSection.remove(); // 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 // --------------------------------------------------------------------------- let _treeDelegationInstalled = false; function _initTreeDelegation() { if (_treeDelegationInstalled) return; const tree = document.getElementById('vault-tree'); if (!tree) { // Retry once when DOM might not be ready โ€” use MutationObserver as fallback const observer = new MutationObserver(() => { const t = document.getElementById('vault-tree'); if (t) { observer.disconnect(); _attachTreeListener(t); } }); observer.observe(document.body, { childList: true, subtree: true }); return; } _attachTreeListener(tree); } function _attachTreeListener(tree) { if (_treeDelegationInstalled) return; _treeDelegationInstalled = true; 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=false`; const data = await api(url); if (!data.files || data.files.length === 0) { container.innerHTML = `

${t('vaulthome.empty')}

`; safeCreateIcons(); return; } // Header const header = el('div', { class: 'vault-home-header' }); const countKey = data.files.length > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count'; const countStr = t(countKey, { count: data.files.length }); const count = el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [ document.createTextNode(countStr), ]); header.appendChild(count); // Directory label const dirLabel = dirPath ? `/${dirPath}` : ` ${t('common.root')}`; 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' }); const locale = getLocale(); data.files.forEach((f) => { const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file'; const displayName = f.name.endsWith('.md') ? f.name.slice(0, -3) : f.name; const ext = f.extension || ''; const titleDiv = el('div', { class: 'search-result-title' }, [ document.createTextNode(displayName), ]); // Format date using current locale const modDate = new Date(f.modified * 1000); const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); // Build metadata line: date + size const metaParts = []; 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) { const errMsg = t('vaulthome.error_loading') + ' ' + escapeHtml(err?.detail || err?.message || t('common.unknown')); container.innerHTML = `

${errMsg}

`; } } // --------------------------------------------------------------------------- // 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 = ` ${t('dashboard.recent')}`; 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.endsWith('.md') ? title.slice(0, -3) : 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 + ' ' + t('vaulthome.size_bytes'); if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' ' + t('vaulthome.size_kb'); return (bytes / (1024 * 1024)).toFixed(1) + ' ' + t('vaulthome.size_mb'); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }