2025-11-11 01:05:13 +01:00
|
|
|
import app from '../app.js';
|
|
|
|
|
|
|
|
|
|
const api = app.getAPI();
|
|
|
|
|
const logger = app.getLogger();
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
class CodeEditorView extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.editor = null;
|
|
|
|
|
this.file = null;
|
|
|
|
|
this.previousView = null;
|
2025-11-11 01:05:13 +01:00
|
|
|
this.isRendered = false;
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('CodeEditorView connected');
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('CodeEditorView disconnected');
|
|
|
|
|
this.destroyEditor();
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
async setFile(file, previousView = 'files') {
|
|
|
|
|
if (this.isRendered) {
|
|
|
|
|
logger.warn('Editor already rendered, skipping');
|
|
|
|
|
return;
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.file = file;
|
|
|
|
|
this.previousView = previousView;
|
2025-11-11 01:05:13 +01:00
|
|
|
this.isRendered = true;
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
try {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('Loading file', { fileName: file.name });
|
|
|
|
|
const blob = await api.downloadFile(file.id);
|
2025-11-10 15:46:40 +01:00
|
|
|
const content = await blob.text();
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
this.createUI(content);
|
|
|
|
|
this.createEditor(content);
|
2025-11-10 15:46:40 +01:00
|
|
|
} catch (error) {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.error('Failed to load file', error);
|
2025-11-10 15:46:40 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Failed to load file: ' + error.message, type: 'error' }
|
|
|
|
|
}));
|
|
|
|
|
window.history.back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
createUI(content) {
|
2025-11-10 15:46:40 +01:00
|
|
|
this.innerHTML = `
|
2025-11-11 01:05:13 +01:00
|
|
|
<div class="code-editor-overlay">
|
2025-11-11 17:57:45 +01:00
|
|
|
<div class="code-editor-header">
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
<button class="button" id="back-btn">Back</button>
|
|
|
|
|
<div class="preview-info">
|
|
|
|
|
<h2 class="preview-file-name">${this.escapeHtml(this.file.name)}</h2>
|
|
|
|
|
<p class="preview-file-info">${this.formatFileSize(this.file.size)} • ${new Date(this.file.created_at).toLocaleDateString()}</p>
|
2025-11-11 01:05:13 +01:00
|
|
|
</div>
|
2025-11-10 15:46:40 +01:00
|
|
|
</div>
|
2025-11-11 17:57:45 +01:00
|
|
|
<div class="preview-actions">
|
|
|
|
|
<button class="button" id="download-btn">Download</button>
|
|
|
|
|
<button class="button button-primary" id="save-btn">Save & Close</button>
|
2025-11-10 15:46:40 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-11 17:57:45 +01:00
|
|
|
<div class="code-editor-body">
|
|
|
|
|
<textarea id="editor-textarea"></textarea>
|
|
|
|
|
</div>
|
2025-11-10 15:46:40 +01:00
|
|
|
</div>
|
|
|
|
|
`;
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
const backBtn = this.querySelector('#back-btn');
|
|
|
|
|
const saveBtn = this.querySelector('#save-btn');
|
2025-11-11 17:57:45 +01:00
|
|
|
const downloadBtn = this.querySelector('#download-btn');
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
backBtn.addEventListener('click', () => this.close());
|
|
|
|
|
saveBtn.addEventListener('click', () => this.save());
|
2025-11-11 17:57:45 +01:00
|
|
|
downloadBtn.addEventListener('click', () => this.downloadFile());
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
createEditor(content) {
|
|
|
|
|
const textarea = this.querySelector('#editor-textarea');
|
|
|
|
|
if (!textarea) {
|
|
|
|
|
logger.error('Textarea not found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea.value = content;
|
|
|
|
|
|
|
|
|
|
const mode = this.getMode(this.file.name);
|
|
|
|
|
logger.debug('Creating CodeMirror editor', { mode, fileSize: content.length });
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
this.editor = CodeMirror.fromTextArea(textarea, {
|
2025-11-11 01:05:13 +01:00
|
|
|
mode: mode,
|
2025-11-10 15:46:40 +01:00
|
|
|
lineNumbers: true,
|
|
|
|
|
lineWrapping: true,
|
|
|
|
|
indentUnit: 4,
|
|
|
|
|
indentWithTabs: false,
|
2025-11-11 01:05:13 +01:00
|
|
|
theme: 'default',
|
|
|
|
|
readOnly: false,
|
|
|
|
|
autofocus: true,
|
2025-11-10 15:46:40 +01:00
|
|
|
extraKeys: {
|
2025-11-11 01:05:13 +01:00
|
|
|
'Ctrl-S': () => { this.save(); return false; },
|
|
|
|
|
'Cmd-S': () => { this.save(); return false; },
|
|
|
|
|
'Esc': () => { this.close(); return false; }
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.editor.setSize('100%', '100%');
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (this.editor) {
|
|
|
|
|
this.editor.refresh();
|
|
|
|
|
this.editor.focus();
|
|
|
|
|
logger.debug('Editor ready and focused');
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMode(filename) {
|
|
|
|
|
const ext = filename.split('.').pop().toLowerCase();
|
|
|
|
|
const modes = {
|
|
|
|
|
'js': 'javascript',
|
|
|
|
|
'json': { name: 'javascript', json: true },
|
|
|
|
|
'py': 'python',
|
|
|
|
|
'md': 'markdown',
|
|
|
|
|
'html': 'htmlmixed',
|
|
|
|
|
'xml': 'xml',
|
|
|
|
|
'css': 'css',
|
|
|
|
|
'txt': 'text/plain',
|
|
|
|
|
'log': 'text/plain',
|
|
|
|
|
'sh': 'shell',
|
|
|
|
|
'yaml': 'yaml',
|
|
|
|
|
'yml': 'yaml'
|
|
|
|
|
};
|
|
|
|
|
return modes[ext] || 'text/plain';
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
escapeHtml(text) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 17:57:45 +01:00
|
|
|
async downloadFile() {
|
|
|
|
|
try {
|
|
|
|
|
const blob = await api.downloadFile(this.file.id);
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = this.file.name;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to download file:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatFileSize(bytes) {
|
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
|
|
|
let size = bytes;
|
|
|
|
|
let unitIndex = 0;
|
|
|
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
|
|
|
size /= 1024;
|
|
|
|
|
unitIndex++;
|
|
|
|
|
}
|
|
|
|
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
handleKeydown(e) {
|
|
|
|
|
if (e.key === 'Escape' && !this.editor.getOption('readOnly')) {
|
|
|
|
|
this.close();
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async save() {
|
2025-11-11 01:05:13 +01:00
|
|
|
if (!this.editor) {
|
|
|
|
|
logger.warn('No editor instance');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveBtn = this.querySelector('#save-btn');
|
|
|
|
|
if (!saveBtn) return;
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
try {
|
2025-11-11 01:05:13 +01:00
|
|
|
saveBtn.disabled = true;
|
|
|
|
|
saveBtn.textContent = 'Saving...';
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
const content = this.editor.getValue();
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('Saving file', { fileName: this.file.name, size: content.length });
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
await api.updateFile(this.file.id, content);
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
logger.info('File saved successfully', { fileName: this.file.name });
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File saved successfully!', type: 'success' }
|
|
|
|
|
}));
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.close();
|
|
|
|
|
}, 500);
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
} catch (error) {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.error('Failed to save file', error);
|
2025-11-10 15:46:40 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
2025-11-11 01:05:13 +01:00
|
|
|
detail: { message: 'Failed to save: ' + error.message, type: 'error' }
|
2025-11-10 15:46:40 +01:00
|
|
|
}));
|
2025-11-11 01:05:13 +01:00
|
|
|
|
|
|
|
|
if (saveBtn) {
|
|
|
|
|
saveBtn.disabled = false;
|
|
|
|
|
saveBtn.textContent = 'Save & Close';
|
|
|
|
|
}
|
2025-11-10 15:46:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 01:05:13 +01:00
|
|
|
close() {
|
|
|
|
|
logger.debug('Closing editor');
|
2025-11-10 15:46:40 +01:00
|
|
|
window.history.back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hide() {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('Hiding editor');
|
|
|
|
|
this.destroyEditor();
|
|
|
|
|
this.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroyEditor() {
|
2025-11-10 15:46:40 +01:00
|
|
|
if (this.editor) {
|
2025-11-11 01:05:13 +01:00
|
|
|
logger.debug('Destroying CodeMirror instance');
|
|
|
|
|
try {
|
|
|
|
|
this.editor.toTextArea();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.warn('Error destroying editor', e);
|
|
|
|
|
}
|
2025-11-10 15:46:40 +01:00
|
|
|
this.editor = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('code-editor-view', CodeEditorView);
|
|
|
|
|
export { CodeEditorView };
|