import { translate } from '../utils/i18nHelpers.js'; /** * Reusable directory picker modal backed by POST /api/lm/browse-directory. * Self-managed (NOT registered with ModalManager): it stacks above the * settings modal, so ModalManager's "close current modal on open" behavior * would kill the modal underneath. */ class DirectoryPickerModal { constructor() { this.isOpen = false; this.currentPath = ''; this.parentPath = null; this.onSelect = null; this.elements = {}; this._bindings = []; } open({ initialPath = '', onSelect } = {}) { this._cacheElements(); if (!this.elements.modal) { console.warn('DirectoryPickerModal: #directoryPickerModal not found in DOM'); return; } this._unbindEvents(); this.onSelect = typeof onSelect === 'function' ? onSelect : null; this.currentPath = ''; this.parentPath = null; this._clearError(); this.elements.folderList.innerHTML = ''; this.elements.currentPathEl.textContent = ''; this.elements.upBtn.disabled = true; this.elements.pathInput.value = initialPath || ''; this._bindEvents(); document.body.classList.add('modal-open'); this.elements.modal.style.display = 'block'; this.isOpen = true; // An empty path lets the server pick its default (user home). this.loadDirectory(initialPath || ''); } close() { if (!this.isOpen) return; this.isOpen = false; this._unbindEvents(); if (this.elements.modal) { this.elements.modal.style.display = 'none'; } this.onSelect = null; // Keep body.modal-open: the settings modal underneath may still be open. } async loadDirectory(path) { try { const response = await fetch('/api/lm/browse-directory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) }); const data = await response.json(); if (data.success) { this._clearError(); this._renderDirectory(data); } else { this._showError(data.error || translate('settings.directoryPicker.loadError', {}, 'Failed to load directory')); } } catch (error) { console.error('Error loading directory:', error); this._showError(translate('settings.directoryPicker.loadError', {}, 'Failed to load directory')); } } _cacheElements() { const modal = document.getElementById('directoryPickerModal'); this.elements = { modal, closeBtn: document.getElementById('directoryPickerCloseBtn'), pathInput: document.getElementById('directoryPickerPathInput'), goBtn: document.getElementById('directoryPickerGoBtn'), upBtn: document.getElementById('directoryPickerUpBtn'), currentPathEl: document.getElementById('directoryPickerCurrentPath'), folderList: document.getElementById('directoryPickerFolderList'), errorEl: document.getElementById('directoryPickerError'), selectBtn: document.getElementById('directoryPickerSelectBtn') }; } _bind(target, type, handler, options) { target.addEventListener(type, handler, options); this._bindings.push([target, type, handler, options]); } _bindEvents() { const { modal, closeBtn, pathInput, goBtn, upBtn, selectBtn } = this.elements; this._bind(closeBtn, 'click', () => this.close()); this._bind(goBtn, 'click', () => this.loadDirectory(pathInput.value.trim())); this._bind(pathInput, 'keydown', (event) => { if (event.key === 'Enter') { this.loadDirectory(pathInput.value.trim()); } }); this._bind(upBtn, 'click', () => { // Server-provided parent_path: Windows paths cannot be derived client-side. if (this.parentPath) { this.loadDirectory(this.parentPath); } }); this._bind(selectBtn, 'click', () => this._selectCurrent()); // Capture phase + stopPropagation so an ESC here never reaches the // settings modal's own ESC handler underneath. this._bind(document, 'keydown', (event) => { if (event.key === 'Escape') { event.stopPropagation(); this.close(); } }, true); // Backdrop click (the .modal element itself, not its content). this._bind(modal, 'click', (event) => { if (event.target === modal) { this.close(); } }); } _unbindEvents() { for (const [target, type, handler, options] of this._bindings) { target.removeEventListener(type, handler, options); } this._bindings = []; } _renderDirectory(data) { this.currentPath = data.current_path || ''; this.parentPath = data.parent_path || null; this.elements.currentPathEl.textContent = this.currentPath; this.elements.pathInput.value = this.currentPath; this.elements.upBtn.disabled = !this.parentPath; const folderList = this.elements.folderList; folderList.innerHTML = ''; const directories = data.directories || []; if (directories.length === 0) { const empty = document.createElement('div'); empty.className = 'directory-picker-empty'; empty.textContent = translate('settings.directoryPicker.emptyFolder', {}, 'This folder is empty'); folderList.appendChild(empty); return; } directories.forEach((entry) => { folderList.appendChild(this._createFolderItem(entry)); }); } // Each entry is { name, path, is_parent }; the server supplies the full // child path, so navigation never joins path segments client-side. _createFolderItem(entry) { const item = document.createElement('div'); item.className = 'folder-item'; item.innerHTML = ` ${this._escapeHtml(entry.name)} `; item.addEventListener('click', () => { this.loadDirectory(entry.path); }); return item; } _selectCurrent() { if (!this.currentPath) return; if (this.onSelect) { this.onSelect(this.currentPath); } this.close(); } _showError(message) { this.elements.errorEl.textContent = message; this.elements.errorEl.style.display = 'block'; } _clearError() { this.elements.errorEl.textContent = ''; this.elements.errorEl.style.display = 'none'; } _escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } export const directoryPickerModal = new DirectoryPickerModal(); export { DirectoryPickerModal };