/* 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 = `
📄
${name}
${Atlus.formatBytes(info.size)} • ${mime || 'Unknown type'}
${info.permissions} • ${info.owner}:${info.group}
`; infoCard.querySelector('.editor-download-btn').addEventListener('click', async () => { const dlRes = await Atlus.apiFetch(`/api/files/download?path=${encodeURIComponent(path)}`); if (dlRes.ok) { const blob = await dlRes.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url); } }); updateStatus(info.size); } } catch (e) { infoCard.innerHTML = `
${name}
Could not load file info
`; } bodyEl.appendChild(infoCard); } // ---- App registration ---- Atlus.registerApp('editor', { title: 'Editor', init(el) { container = el; container.classList.add('app-editor'); // Toolbar const toolbar = document.createElement('div'); toolbar.className = 'editor-toolbar'; titleEl = document.createElement('div'); titleEl.className = 'editor-title'; titleEl.textContent = 'No file open'; dirtyEl = document.createElement('span'); dirtyEl.className = 'editor-dirty hidden'; dirtyEl.textContent = '●'; dirtyEl.title = 'Unsaved changes'; const titleWrap = document.createElement('div'); titleWrap.className = 'editor-title-wrap'; titleWrap.appendChild(titleEl); titleWrap.appendChild(dirtyEl); saveBtn = document.createElement('button'); saveBtn.className = 'editor-save-btn hidden'; saveBtn.textContent = 'Save'; saveBtn.addEventListener('click', saveFile); toolbar.appendChild(titleWrap); toolbar.appendChild(saveBtn); container.appendChild(toolbar); // Body bodyEl = document.createElement('div'); bodyEl.className = 'editor-content'; container.appendChild(bodyEl); // Status bar statusEl = document.createElement('div'); statusEl.className = 'editor-status'; container.appendChild(statusEl); // Check for pending file open if (container._pendingFile) { const { path, mime } = container._pendingFile; delete container._pendingFile; openFile(path, mime); } }, destroy() { // Revoke any blob URLs if (bodyEl) { const imgs = bodyEl.querySelectorAll('img'); imgs.forEach(img => { if (img.src.startsWith('blob:')) URL.revokeObjectURL(img.src); }); } container = null; editorEl = null; linesEl = null; titleEl = null; dirtyEl = null; saveBtn = null; statusEl = null; bodyEl = null; currentFile = null; isDirty = false; }, // Public API for file manager integration openFile(path, mime) { if (container) { openFile(path, mime); } }, }); })();