/* Atlus — Text Editor / File Viewer app */ (function () { 'use strict'; let container = null; let currentFile = null; // { path, name, mime, size } let isDirty = false; let editorEl = null; // textarea element let linesEl = null; // line numbers element let titleEl = null; // filename display let dirtyEl = null; // dirty indicator let saveBtn = null; let statusEl = null; let bodyEl = null; // main content area // MIME types we treat as text/editable const TEXT_MIMES = [ 'text/', 'application/json', 'application/javascript', 'application/xml', 'application/x-sh', 'application/x-python', 'application/toml', 'application/yaml', 'application/x-yaml', 'application/sql', 'application/x-httpd-php', ]; const IMAGE_MIMES = ['image/']; function isTextMime(mime) { if (!mime) return true; // Unknown = try text return TEXT_MIMES.some(t => mime.startsWith(t)); } function isImageMime(mime) { if (!mime) return false; return IMAGE_MIMES.some(t => mime.startsWith(t)); } // ---- Line numbers ---- function updateLineNumbers() { if (!editorEl || !linesEl) return; const count = editorEl.value.split('\n').length; const nums = []; for (let i = 1; i <= count; i++) nums.push(i); linesEl.textContent = nums.join('\n'); } function syncScroll() { if (linesEl && editorEl) { linesEl.scrollTop = editorEl.scrollTop; } } // ---- Dirty tracking ---- function markDirty() { if (!isDirty) { isDirty = true; if (dirtyEl) dirtyEl.classList.remove('hidden'); } } function markClean() { isDirty = false; if (dirtyEl) dirtyEl.classList.add('hidden'); } // ---- Save ---- async function saveFile() { if (!currentFile || !editorEl) return; saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; try { const res = await Atlus.apiFetch('/api/files/write', { method: 'POST', body: { path: currentFile.path, content: editorEl.value }, }); if (res.ok) { markClean(); const data = await res.json(); updateStatus(data.size); } } catch (e) { // show error inline } saveBtn.disabled = false; saveBtn.textContent = 'Save'; } // ---- Status bar ---- function updateStatus(size) { if (!statusEl) return; const parts = []; if (currentFile) { if (editorEl) { const lines = editorEl.value.split('\n').length; parts.push(`${lines} lines`); } if (size !== undefined) { parts.push(Atlus.formatBytes(size)); } if (currentFile.mime) { parts.push(currentFile.mime); } } statusEl.textContent = parts.join(' • '); } // ---- Open file ---- async function openFile(path, mime) { const name = path.split('/').pop(); currentFile = { path, name, mime, size: 0 }; isDirty = false; // Update title if (titleEl) titleEl.textContent = name; if (dirtyEl) dirtyEl.classList.add('hidden'); // Clear body bodyEl.innerHTML = ''; if (isImageMime(mime)) { await renderImage(path, name); } else if (isTextMime(mime)) { await renderTextEditor(path); } else { await renderBinaryInfo(path, name, mime); } } async function renderTextEditor(path) { // Create editor layout const editorWrap = document.createElement('div'); editorWrap.className = 'editor-body'; linesEl = document.createElement('div'); linesEl.className = 'editor-lines'; editorEl = document.createElement('textarea'); editorEl.className = 'editor-textarea'; editorEl.spellcheck = false; editorEl.autocapitalize = 'off'; editorEl.autocomplete = 'off'; editorWrap.appendChild(linesEl); editorWrap.appendChild(editorEl); bodyEl.appendChild(editorWrap); // Show save button if (saveBtn) saveBtn.classList.remove('hidden'); // Load content try { const res = await Atlus.apiFetch(`/api/files/read?path=${encodeURIComponent(path)}`); if (res.ok) { const data = await res.json(); editorEl.value = data.content; currentFile.size = data.content.length; updateLineNumbers(); updateStatus(currentFile.size); } else { editorEl.value = `Error loading file: ${res.status}`; } } catch (e) { editorEl.value = `Error: ${e.message}`; } // Events editorEl.addEventListener('input', () => { markDirty(); updateLineNumbers(); }); editorEl.addEventListener('scroll', syncScroll); // Tab key support editorEl.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); const start = editorEl.selectionStart; const end = editorEl.selectionEnd; editorEl.value = editorEl.value.substring(0, start) + ' ' + editorEl.value.substring(end); editorEl.selectionStart = editorEl.selectionEnd = start + 4; markDirty(); updateLineNumbers(); } // Ctrl+S / Cmd+S if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveFile(); } }); } async function renderImage(path, name) { if (saveBtn) saveBtn.classList.add('hidden'); linesEl = null; editorEl = null; const imgWrap = document.createElement('div'); imgWrap.className = 'editor-preview'; const img = document.createElement('img'); img.className = 'editor-preview-img'; img.alt = name; // Load via authenticated fetch → blob URL try { const res = await Atlus.apiFetch(`/api/files/download?path=${encodeURIComponent(path)}`); if (res.ok) { const blob = await res.blob(); img.src = URL.createObjectURL(blob); currentFile.size = blob.size; updateStatus(blob.size); } } catch (e) { img.alt = 'Failed to load image'; } imgWrap.appendChild(img); bodyEl.appendChild(imgWrap); } async function renderBinaryInfo(path, name, mime) { if (saveBtn) saveBtn.classList.add('hidden'); linesEl = null; editorEl = null; const infoCard = document.createElement('div'); infoCard.className = 'editor-binary-info'; // Fetch file info try { const res = await Atlus.apiFetch(`/api/files/info?path=${encodeURIComponent(path)}`); if (res.ok) { const info = await res.json(); currentFile.size = info.size; infoCard.innerHTML = `