import { render } from 'solid-js/web'; import { createSignal, onMount, For, Show, onCleanup, createEffect } from 'solid-js'; import hljs from 'highlight.js/lib/core'; import javascript_hljs from 'highlight.js/lib/languages/javascript'; import typescript_hljs from 'highlight.js/lib/languages/typescript'; import json_hljs from 'highlight.js/lib/languages/json'; import xml_hljs from 'highlight.js/lib/languages/xml'; import css_hljs from 'highlight.js/lib/languages/css'; import bash_hljs from 'highlight.js/lib/languages/bash'; import markdown_hljs from 'highlight.js/lib/languages/markdown'; import sql_hljs from 'highlight.js/lib/languages/sql'; import { EditorView, keymap, lineNumbers } from '@codemirror/view'; import { EditorState, Compartment } from '@codemirror/state'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle, foldGutter, foldKeymap } from '@codemirror/language'; import { lintGutter } from "@codemirror/lint"; import { oneDark } from '@codemirror/theme-one-dark'; import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; import { html } from '@codemirror/lang-html'; import { css } from '@codemirror/lang-css'; import { markdown } from '@codemirror/lang-markdown'; hljs.registerLanguage('javascript', javascript_hljs); hljs.registerLanguage('typescript', typescript_hljs); hljs.registerLanguage('json', json_hljs); hljs.registerLanguage('html', xml_hljs); hljs.registerLanguage('css', css_hljs); hljs.registerLanguage('bash', bash_hljs); hljs.registerLanguage('markdown', markdown_hljs); hljs.registerLanguage('sql', sql_hljs); function App() { const [loading, setLoading] = createSignal(true); const [status, setStatus] = createSignal(''); const [notifications, setNotifications] = createSignal([]); const [userData, setUserData] = createSignal(null); const [files, setFiles] = createSignal([]); const [selectedFile, setSelectedFile] = createSignal(null); const [fileContent, setFileContent] = createSignal(''); const [newFileName, setNewFileName] = createSignal(''); const [newFolderName, setNewFolderName] = createSignal(''); const [openFolders, setOpenFolders] = createSignal({}); const [contextMenu, setContextMenu] = createSignal({ visible: false, x: 0, y: 0, file: null, }); const [isMobileView, setIsMobileView] = createSignal(false); const [isEditingFile, setIsEditingFile] = createSignal(true); const [initialLoadComplete, setInitialLoadComplete] = createSignal(false); const [renameContainerInfo, setRenameContainerInfo] = createSignal({ visible: false, file: null, newName: '', }); const [createItemModalInfo, setCreateItemModalInfo] = createSignal({ visible: false, parentPath: null, isDir: false, itemName: '', }); let renameInputRef; let codeRef; let createItemInputRef; let editorRef; let editorViewInstance = null; const languageCompartment = new Compartment(); const themeCompartment = new Compartment(); const editableCompartment = new Compartment(); const linkManager = '/private/server/exocore/web/file'; const getToken = () => localStorage.getItem('exocore-token') || ''; const getCookies = () => localStorage.getItem('exocore-cookies') || ''; const addNotification = (message, type = 'info', duration = 4000) => { const id = Date.now(); setNotifications(prev => [...prev, { id, message, type }]); setTimeout(() => { setNotifications(prev => prev.filter(n => n.id !== id)); }, duration); }; function sortFileSystemItems(items) { if (!Array.isArray(items)) return []; const specialOrder = ['.git', 'package.json', 'package-lock.json']; const nodeModulesName = 'node_modules'; const regularItems = items.filter(item => !specialOrder.includes(item.name) && item.name !== nodeModulesName); const specialItemsOnList = items.filter(item => specialOrder.includes(item.name)); const nodeModulesItem = items.find(item => item.name === nodeModulesName); regularItems.sort((a, b) => { if (a.isDir && !b.isDir) return -1; if (!a.isDir && b.isDir) return 1; return a.name.localeCompare(b.name); }); specialItemsOnList.sort((a, b) => specialOrder.indexOf(a.name) - specialOrder.indexOf(b.name)); const sortedItems = [...regularItems, ...specialItemsOnList]; if (nodeModulesItem) { sortedItems.push(nodeModulesItem); } return sortedItems; } async function fetchUserInfo() { setLoading(true); const token = getToken(); const cookies = getCookies(); if (!token || !cookies) { setLoading(false); setInitialLoadComplete(true); window.location.href = '/private/server/exocore/web/public/login'; return; } try { const res = await fetch('/private/server/exocore/web/userinfo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, cookies }), }); if (!res.ok) { let errorMsg = `Server error: ${res.status}`; try { const errorData = await res.json(); errorMsg = errorData.message || errorMsg; } catch (parseError) {} throw new Error(errorMsg); } const data = await res.json(); if (data.data?.user && data.data.user.verified === 'success') { setUserData(data.data.user); await fetchFiles(''); } else { setUserData(null); const redirectMsg = data.message || 'User verification failed. Redirecting to login...'; setStatus(redirectMsg); localStorage.removeItem('exocore-token'); localStorage.removeItem('exocore-cookies'); setTimeout(() => { window.location.href = '/private/server/exocore/web/public/login'; }, 2500); } } catch (err) { setUserData(null); const errorMessage = err instanceof Error ? err.message : String(err); const redirectMsg = 'Failed to fetch user info: ' + errorMessage + '. Redirecting to login...'; setStatus(redirectMsg); localStorage.removeItem('exocore-token'); localStorage.removeItem('exocore-cookies'); setTimeout(() => { window.location.href = '/private/server/exocore/web/public/login'; }, 2500); } finally { setLoading(false); setInitialLoadComplete(true); } } async function fetchFiles(currentPath = '') { setLoading(true); let endpoint = ''; let bodyPayload = {}; if (currentPath) { endpoint = `${linkManager}/open-folder`; bodyPayload = { folder: currentPath, }; } else { endpoint = `${linkManager}/list`; bodyPayload = {}; } try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(bodyPayload), }); const responseText = await res.text(); if (!res.ok) { let errorMsg = responseText; try { const errData = JSON.parse(responseText); errorMsg = errData.message || errData.error || responseText; } catch (e) {} throw new Error(errorMsg || `HTTP error! status: ${res.status}`); } const data = JSON.parse(responseText); if (currentPath) { if (data && Array.isArray(data.items)) { setOpenFolders((prev) => ({ ...prev, [currentPath]: sortFileSystemItems(data.items), })); } else { setOpenFolders((prev) => ({ ...prev, [currentPath]: [] })); addNotification(`Error: Could not load content for folder ${currentPath}.`, 'error'); } } else { if (Array.isArray(data)) { setFiles(sortFileSystemItems(data)); } else { setFiles([]); addNotification(`Error: Could not load root directory.`, 'error'); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification(`Failed to list ${currentPath || 'root'}: ${errorMessage}`, 'error'); if (currentPath) { setOpenFolders((prev) => ({ ...prev, [currentPath]: undefined })); } } finally { setLoading(false); } } async function openFile(file) { setLoading(true); try { const res = await fetch(`${linkManager}/open`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ file, }), }); const text = await res.text(); if (!res.ok) throw new Error(text || `HTTP error! status: ${res.status}`); setSelectedFile(file); setFileContent(text); setIsEditingFile(true); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to open file: ' + errorMessage, 'error'); } finally { setLoading(false); } } function closeFileEditor() { setSelectedFile(null); setFileContent(''); } async function saveFile() { if (!selectedFile()) return; setLoading(true); try { const res = await fetch(`${linkManager}/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ file: selectedFile(), content: fileContent(), }), }); const message = await res.text(); if (!res.ok) throw new Error(message || `HTTP error! status: ${res.status}`); addNotification(message, 'success'); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to save file: ' + errorMessage, 'error'); } finally { setLoading(false); } } async function refreshFileSystem(affectedItemPath = '') { let parentPath = ''; if (affectedItemPath) { const lastSlashIndex = affectedItemPath.lastIndexOf('/'); if (lastSlashIndex !== -1) { parentPath = affectedItemPath.substring(0, lastSlashIndex); } } await fetchFiles(); if (parentPath && openFolders()[parentPath]) { await fetchFiles(parentPath); } } function fileOrFolderNameIsDirectory(path) { if (openFolders()[path] !== undefined) return true; const checkList = (list, currentBuildPath = '') => { for (const item of list) { const itemFullPath = currentBuildPath ? `${currentBuildPath}/${item.name}` : item.name; if (itemFullPath === path) return item.isDir; if (item.isDir && openFolders()[itemFullPath]) { const foundInSub = checkList(openFolders()[itemFullPath], itemFullPath); if (foundInSub !== undefined) return foundInSub; } } return undefined; }; return checkList(files()) || false; } async function createFile() { const name = newFileName().trim(); if (!name) { addNotification('Please enter a file name.', 'error'); return; } setLoading(true); try { const res = await fetch(`${linkManager}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: name }), }); const message = await res.text(); if (!res.ok) throw new Error(message || `HTTP error! status: ${res.status}`); setNewFileName(''); await refreshFileSystem(name); addNotification(`File "${name}" created successfully.`, 'success'); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to create file: ' + errorMessage, 'error'); } finally { setLoading(false); } } async function createFolder() { const name = newFolderName().trim(); if (!name) { addNotification('Please enter a folder name.', 'error'); return; } setLoading(true); try { const res = await fetch(`${linkManager}/create-folder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder: name }), }); const message = await res.text(); if (!res.ok) throw new Error(message || `HTTP error! status: ${res.status}`); setNewFolderName(''); await refreshFileSystem(name); addNotification(`Folder "${name}" created successfully.`, 'success'); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to create folder: ' + errorMessage, 'error'); } finally { setLoading(false); } } async function uploadFile(e) { const fileToUpload = e.target.files[0]; if (!fileToUpload) return; const targetPathForUpload = fileToUpload.name; const form = new FormData(); form.append('file', fileToUpload); setLoading(true); try { const res = await fetch(`${linkManager}/upload`, { method: 'POST', body: form, }); const message = await res.text(); if (!res.ok) throw new Error(message || `HTTP error! status: ${res.status}`); await refreshFileSystem(targetPathForUpload); addNotification(`File "${fileToUpload.name}" uploaded successfully.`, 'success'); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to upload file: ' + errorMessage, 'error'); } finally { setLoading(false); e.target.value = null; } } function download(file) { const form = document.createElement('form'); form.method = 'POST'; form.action = `${linkManager}/download`; form.style.display = 'none'; const input = document.createElement('input'); input.name = 'file'; input.value = file; input.type = 'hidden'; form.appendChild(input); document.body.appendChild(form); form.submit(); document.body.removeChild(form); addNotification(`Downloading "${file}"...`, 'info'); } function toggleFolder(folderPath) { if (openFolders()[folderPath]) { setOpenFolders((prev) => { const updated = { ...prev }; delete updated[folderPath]; Object.keys(updated).forEach(key => { if (key.startsWith(folderPath + '/')) { delete updated[key]; } }); return updated; }); } else { fetchFiles(folderPath); } } function handleFileClick(file, fullPath) { if (file.isDir) { toggleFolder(fullPath); } else { openFile(fullPath); } } function handleContextMenu(e, file, fullPath) { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: e.clientX, y: e.clientY, file: { ...file, path: fullPath }, }); } function handleOpenFolderFromContextMenu() { const folderToOpen = contextMenu().file; if (folderToOpen && folderToOpen.isDir) { if (!openFolders()[folderToOpen.path]) { fetchFiles(folderToOpen.path); } } setContextMenu({ visible: false, x: 0, y: 0, file: null }); } function handleDownloadSelected() { if (contextMenu().file && !contextMenu().file.isDir) { download(contextMenu().file.path); } setContextMenu({ visible: false, x: 0, y: 0, file: null }); } async function handleUnzipSelected() { const fileToUnzip = contextMenu().file; if (!fileToUnzip || fileToUnzip.isDir || !fileToUnzip.name.toLowerCase().endsWith('.zip')) { setContextMenu({ visible: false, x: 0, y: 0, file: null }); return; } const zipFilePath = fileToUnzip.path; setLoading(true); setContextMenu({ visible: false, x: 0, y: 0, file: null }); try { const res = await fetch(`${linkManager}/unzip`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ zipFilePath: zipFilePath, overwrite: true, destinationPath: '', }), }); const message = await res.text(); if (!res.ok) { let errorMsg = message; try { const errData = JSON.parse(message); errorMsg = errData.message || errData.error || message; } catch (e) { } throw new Error(errorMsg || `HTTP error! status: ${res.status}`); } addNotification(message, 'success'); await refreshFileSystem(zipFilePath); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification(`Failed to unzip "${fileToUnzip.name}": ${errorMessage}`, 'error'); } finally { setLoading(false); } } function handleRenameClick() { if (contextMenu().file) { setRenameContainerInfo({ visible: true, file: contextMenu().file, newName: contextMenu().file.name, }); } setContextMenu({ visible: false, x: 0, y: 0, file: null }); } async function handleDeleteSelected() { const fileToDelete = contextMenu().file; if (!fileToDelete) return; const confirmation = window.confirm(`Are you sure you want to delete "${fileToDelete.name}"? This action cannot be undone.`); if (!confirmation) { setContextMenu({ visible: false, x: 0, y: 0, file: null }); return; } setLoading(true); setContextMenu({ visible: false, x: 0, y: 0, file: null }); try { const res = await fetch(`${linkManager}/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: fileToDelete.path }), }); const message = await res.text(); if (!res.ok) { let errorMsg = message; try { const errData = JSON.parse(message); errorMsg = errData.message || errData.error || message; } catch (e) { } throw new Error(errorMsg || `HTTP error! status: ${res.status}`); } addNotification(message || `Successfully deleted "${fileToDelete.name}".`, 'success'); if (selectedFile() && selectedFile().startsWith(fileToDelete.path)) { closeFileEditor(); } if (fileToDelete.isDir) { setOpenFolders((prev) => { const updated = { ...prev }; delete updated[fileToDelete.path]; Object.keys(updated).forEach(key => { if (key.startsWith(fileToDelete.path + '/')) { delete updated[key]; } }); return updated; }); } await refreshFileSystem(fileToDelete.path); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification(`Failed to delete "${fileToDelete.name}": ${errorMessage}`, 'error'); } finally { setLoading(false); } } function cancelRename() { setRenameContainerInfo({ visible: false, file: null, newName: '' }); } async function performRename() { const fileToRename = renameContainerInfo().file; const newName = renameContainerInfo().newName.trim(); if (!fileToRename || !newName) { addNotification('Invalid rename operation. New name cannot be empty.', 'error'); cancelRename(); return; } setLoading(true); try { const oldPath = fileToRename.path; const parentPath = oldPath.substring(0, oldPath.lastIndexOf('/')); const newPath = parentPath ? `${parentPath}/${newName}` : newName; if (oldPath === newPath) { addNotification('No change detected. Renaming cancelled.', 'info'); cancelRename(); setLoading(false); return; } const res = await fetch(`${linkManager}/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldPath, newPath }), }); const message = await res.text(); if (!res.ok) { let errorMsg = message; try { const errData = JSON.parse(message); errorMsg = errData.message || errData.error || message; } catch (e) { } throw new Error(errorMsg || `HTTP error! status: ${res.status}`); } addNotification(`Renamed "${fileToRename.name}" to "${newName}" successfully.`, 'success'); if (fileToRename.isDir && openFolders()[oldPath]) { const contents = openFolders()[oldPath]; setOpenFolders((prev) => { const updated = { ...prev }; delete updated[oldPath]; updated[newPath] = contents; Object.keys(updated).forEach(key => { if (key.startsWith(oldPath + '/')) { const subPath = key.substring(oldPath.length); const oldSubOpenFolderContent = updated[key]; delete updated[key]; updated[newPath + subPath] = oldSubOpenFolderContent; } }); return updated; }); } if (selectedFile() === oldPath) { setSelectedFile(newPath); } await refreshFileSystem(newPath); cancelRename(); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification('Failed to rename: ' + errorMessage, 'error'); } finally { setLoading(false); } } function handleShowCreateItemModal(isDirContext) { const contextFile = contextMenu().file; let parentPathTarget = ''; if (contextFile) { if (contextFile.isDir) { parentPathTarget = contextFile.path; } else { const lastSlash = contextFile.path.lastIndexOf('/'); parentPathTarget = lastSlash === -1 ? '' : contextFile.path.substring(0, lastSlash); } } setCreateItemModalInfo({ visible: true, parentPath: parentPathTarget, isDir: isDirContext, itemName: '', }); setContextMenu({ visible: false, x: 0, y: 0, file: null }); } function cancelCreateItem() { setCreateItemModalInfo({ visible: false, parentPath: null, isDir: false, itemName: '' }); } async function performCreateItem() { const { parentPath, itemName, isDir } = createItemModalInfo(); const newItemNameTrimmed = itemName.trim(); if (!newItemNameTrimmed) { addNotification(`Please enter a ${isDir ? 'folder' : 'file'} name.`, 'error'); return; } const fullPath = parentPath ? `${parentPath}/${newItemNameTrimmed}` : newItemNameTrimmed; setLoading(true); try { const endpoint = isDir ? `${linkManager}/create-folder` : `${linkManager}/create`; const payload = isDir ? { folder: fullPath } : { file: fullPath }; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const message = await res.text(); if (!res.ok) { let errorMsg = message; try { const errData = JSON.parse(message); errorMsg = errData.message || errData.error || message; } catch (e) { } throw new Error(errorMsg || `HTTP error! status: ${res.status}`); } addNotification(`${isDir ? 'Folder' : 'File'} "${newItemNameTrimmed}" created successfully in "${parentPath || 'root'}".`, 'success'); cancelCreateItem(); await refreshFileSystem(fullPath); if (parentPath) { await fetchFiles(parentPath); } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); addNotification(`Failed to create ${isDir ? 'folder' : 'file'}: ${errorMessage}`, 'error'); } finally { setLoading(false); } } function getFileIconPath(fileItem, isFolderOpen) { const baseIconPath = './icons/'; const nameLower = fileItem.name.toLowerCase(); if (fileItem.isDir) { if (nameLower === '.git') { return `${baseIconPath}git.svg`; } return isFolderOpen ? `${baseIconPath}folder-open.svg` : `${baseIconPath}folder.svg`; } if (nameLower === 'exocore.run') { return `${baseIconPath}exocore.run.svg`; } if (nameLower === '.gitignore' || nameLower === '.gitattributes' || nameLower === '.gitmodules') { return `${baseIconPath}git.svg`; } const parts = nameLower.split('.'); let extension = ''; if (parts.length > 1) { const potentialExtension = parts.pop(); if (parts[0] !== '' || parts.length > 0) { if (potentialExtension !== undefined) { extension = potentialExtension; } } else if (potentialExtension !== undefined) { extension = potentialExtension; } } switch (extension) { case 'js': return `${baseIconPath}js.svg`; case 'jsx': return `${baseIconPath}jsx.svg`; case 'ts': return `${baseIconPath}ts.svg`; case 'tsx': return `${baseIconPath}tsx.svg`; case 'json': return `${baseIconPath}json.svg`; case 'xml': return `${baseIconPath}xml.svg`; case 'html': return `${baseIconPath}html.svg`; case 'css': return `${baseIconPath}css.svg`; case 'md': return `${baseIconPath}md.svg`; case 'sh': return `${baseIconPath}sh.svg`; case 'sql': return `${baseIconPath}sql.svg`; case 'zip': return `${baseIconPath}zip.svg`; case 'gif': return `${baseIconPath}gifImage.svg`; case 'jpg': case 'jpeg': case 'png': return `${baseIconPath}image.svg`; case 'mp4': case 'mov': case 'avi': case 'mkv': case 'webm': case 'flv': case 'wmv': return `${baseIconPath}video.svg`; case 'git': return `${baseIconPath}git.svg`; default: return `${baseIconPath}undefined.svg`; } } function renderFiles(list, parentPath = '') { return ( ); } function handleDownloadAll() { const form = document.createElement('form'); form.method = 'POST'; form.action = `${linkManager}/download-zip`; form.style.display = 'none'; document.body.appendChild(form); form.submit(); document.body.removeChild(form); addNotification('Downloading all files (root) as ZIP...', 'info'); } const getCodeMirrorLanguageSupport = (filename) => { const extension = filename?.split('.').pop()?.toLowerCase(); if (!extension) return javascript(); switch (extension) { case 'js': case 'jsx': return javascript(); case 'ts': case 'tsx': return javascript({typescript: true, jsx: true}); case 'json': return json(); case 'html': case 'htm': case 'xml': case 'svg': return html(); case 'css': return css(); case 'md': case 'markdown': return markdown(); default: return javascript(); } }; onMount(() => { const patrickHandFontLink = document.createElement('link'); patrickHandFontLink.href = 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap'; patrickHandFontLink.rel = 'stylesheet'; document.head.appendChild(patrickHandFontLink); const firaCodeFontLink = document.createElement('link'); firaCodeFontLink.href = 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap'; firaCodeFontLink.rel = 'stylesheet'; document.head.appendChild(firaCodeFontLink); const hljsThemeLink = document.createElement('link'); hljsThemeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css'; hljsThemeLink.rel = 'stylesheet'; document.head.appendChild(hljsThemeLink); const appElement = document.getElementById('app'); if (appElement) { appElement.style.fontFamily = theme.fontFamily; appElement.style.minHeight = '100vh'; } fetchUserInfo(); const handleClickOutside = (e) => { const contextMenuElement = document.getElementById('context-menu'); const createItemModalOverlayElement = document.querySelector('.create-item-modal-overlay'); const renameModalOverlayElement = document.querySelector('.rename-modal-overlay'); if (contextMenu().visible && contextMenuElement && !contextMenuElement.contains(e.target)) { setContextMenu({ visible: false, x: 0, y: 0, file: null }); } if (createItemModalInfo().visible && createItemModalOverlayElement && e.target === createItemModalOverlayElement) { cancelCreateItem(); } if (renameContainerInfo().visible && renameModalOverlayElement && e.target === renameModalOverlayElement) { cancelRename(); } }; document.addEventListener('click', handleClickOutside); const checkMobile = () => setIsMobileView(window.innerWidth < 768); checkMobile(); window.addEventListener('resize', checkMobile); onCleanup(() => { window.removeEventListener('resize', checkMobile); document.removeEventListener('click', handleClickOutside); if (editorViewInstance) { editorViewInstance.destroy(); editorViewInstance = null; } }); }); createEffect(() => { const currentFile = selectedFile(); const editing = isEditingFile(); const content = fileContent(); if (editorRef && editing && currentFile) { const cmLanguageSupport = getCodeMirrorLanguageSupport(currentFile); if (editorViewInstance) { if (editorViewInstance.state.doc.toString() !== content) { editorViewInstance.dispatch({ changes: { from: 0, to: editorViewInstance.state.doc.length, insert: content || '' } }); } editorViewInstance.dispatch({ effects: [ languageCompartment.reconfigure(cmLanguageSupport), editableCompartment.reconfigure(EditorView.editable.of(true)), ] }); } else { const state = EditorState.create({ doc: content || '', extensions: [ EditorView.lineWrapping, history(), keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap]), lineNumbers(), foldGutter(), lintGutter(), editableCompartment.of(EditorView.editable.of(true)), languageCompartment.of(cmLanguageSupport), themeCompartment.of(oneDark), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), EditorView.theme({ '&': { fontSize: '12px' }, '.cm-content': { fontFamily: theme.monospaceFontFamily }, '.cm-gutters': { fontSize: '13px', backgroundColor: '#282c34' }, '.cm-lineNumbers .cm-gutterElement': { padding: '0 3px 0 5px', minWidth: '20px', textAlign: 'right' } }), EditorView.updateListener.of(update => { if (update.docChanged) { if (update.transactions.some(tr => tr.isUserEvent('input') || tr.isUserEvent('delete'))) { setFileContent(update.state.doc.toString()); } } }) ] }); editorViewInstance = new EditorView({ state, parent: editorRef }); } } else if (editorViewInstance) { editorViewInstance.destroy(); editorViewInstance = null; } }); let hasFocusedRenameInput = false; createEffect(() => { if (renameContainerInfo().visible && renameInputRef && !hasFocusedRenameInput) { setTimeout(() => { if (renameInputRef) { renameInputRef.focus(); renameInputRef.select(); } hasFocusedRenameInput = true; }, 50); } else if (!renameContainerInfo().visible) { hasFocusedRenameInput = false; } }); let hasFocusedCreateItemInput = false; createEffect(() => { if (createItemModalInfo().visible && createItemInputRef && !hasFocusedCreateItemInput) { setTimeout(() => { if (createItemInputRef) { createItemInputRef.focus(); } hasFocusedCreateItemInput = true; }, 50); } else if (!createItemModalInfo().visible) { hasFocusedCreateItemInput = false; } }); const theme = { bg: '#0F172A', panelBg: '#1E293B', border: '#334155', text: '#E2E8F0', textMuted: '#94A3B8', primary: '#38BDF8', primaryHover: '#0EA5E9', primaryText: '#0F172A', secondary: '#FACC15', secondaryHover: '#EAB308', secondaryText: '#1E293B', destructive: '#F43F5E', destructiveHover: '#E11D48', destructiveText: '#E2E8F0', inputBg: '#0A0F1A', inputBorder: '#334155', inputFocusBorder: '#38BDF8', fontFamily: "'Patrick Hand', cursive", monospaceFontFamily: "'Fira Code', 'Source Code Pro', monospace", borderRadius: '6px', itemHoverBg: 'rgba(51, 65, 85, 0.7)', shadow: '0 6px 16px rgba(0, 0, 0, 0.4)', itemSelectedBg: '#38BDF8', notificationSuccess: '#10B981', notificationError: '#F43F5E', notificationInfo: '#38BDF8', }; const NotificationContainer = () => { const baseStyle = { padding: '1rem 1.5rem', 'margin-bottom': '0.75rem', 'border-radius': theme.borderRadius, color: 'white', 'font-size': '1.05rem', 'box-shadow': '0 4px 10px rgba(0,0,0,0.3)', 'font-family': theme.fontFamily, 'letter-spacing': '0.5px', transition: 'transform 0.3s ease-out, opacity 0.3s ease-out', transform: 'translateX(0)', opacity: 1, }; const typeStyles = { success: { 'background-color': theme.notificationSuccess }, error: { 'background-color': theme.notificationError }, info: { 'background-color': theme.notificationInfo, color: theme.primaryText }, }; return (
{notification => (
{notification.message}
)}
); }; const baseButtonStyle = { padding: '0.6rem 1.2rem', 'font-size': '1.1rem', border: 'none', 'border-radius': theme.borderRadius, cursor: 'pointer', 'font-family': theme.fontFamily, 'letter-spacing': '0.5px', transition: 'background-color 0.2s, transform 0.1s', 'margin-right': '0.5rem', 'line-height': '1.4', }; const createButtonStyler = (baseColor, hoverColor, textColor) => { let btnRef; return { ref: el => btnRef = el, style: { ...baseButtonStyle, background: baseColor, color: textColor }, onMouseEnter: () => btnRef && (btnRef.style.backgroundColor = hoverColor), onMouseLeave: () => btnRef && (btnRef.style.backgroundColor = baseColor), }; }; const primaryButtonProps = (text, onClick) => { const { ref, style, onMouseEnter, onMouseLeave } = createButtonStyler(theme.primary, theme.primaryHover, theme.primaryText); return ; }; const secondaryButtonProps = (text, onClick) => { const { ref, style, onMouseEnter, onMouseLeave } = createButtonStyler(theme.secondary, theme.secondaryHover, theme.secondaryText); return ; }; const destructiveButtonProps = (text, onClick) => { const { ref, style, onMouseEnter, onMouseLeave } = createButtonStyler(theme.destructive, theme.destructiveHover, theme.destructiveText); return ; }; const defaultButtonProps = (text, onClick, additionalStyles = {}) => { const { ref, style, onMouseEnter, onMouseLeave } = createButtonStyler(theme.panelBg, theme.border, theme.textMuted); return ; }; const iconButtonStyler = (baseColor, hoverColor, textColor) => { let btnRef; const iconBaseStyle = { ...baseButtonStyle, padding: '0.5rem 0.7rem', 'font-size': '1.5rem', 'line-height': '1', 'margin-right': '0' }; return { ref: el => btnRef = el, style: { ...iconBaseStyle, background: baseColor, color: textColor, border: `1px solid ${theme.border}`}, onMouseEnter: () => btnRef && (btnRef.style.backgroundColor = hoverColor), onMouseLeave: () => btnRef && (btnRef.style.backgroundColor = baseColor), }; }; const inputStyle = { padding: '0.7rem 0.9rem', 'font-size': '1.05rem', border: `1px solid ${theme.inputBorder}`, 'border-radius': theme.borderRadius, flex: '1', 'margin-right': '0.5rem', 'font-family': theme.fontFamily, 'background-color': theme.inputBg, color: theme.text, outline: 'none', transition: 'border-color 0.2s, box-shadow 0.2s', 'letter-spacing': '0.5px', }; const modalInputStyle = { ...inputStyle, width: 'calc(100% - 22px)', 'margin-bottom': '20px', 'margin-right': '0', 'font-size': '1.1rem', }; const codeViewerBaseStyle = { width: '100%', fontFamily: theme.monospaceFontFamily, border: `1px solid ${theme.border}`, padding: '15px', boxSizing: 'border-box', flex: '1', marginBottom: '15px', minHeight: isMobileView() ? '250px' : '350px', resize: 'vertical', overflow: 'auto', backgroundColor: '#282c34', color: theme.text, borderRadius: theme.borderRadius, fontSize: '12px', }; const codeMirrorEditorStyle = { ...codeViewerBaseStyle, padding: '0px', }; return (

📁 ExoCore Explorer 📂

{status()} {defaultButtonProps('Clear', () => setStatus(''), { padding: '0.2rem 0.5rem', 'margin-left': '10px', 'font-size': '0.9rem' })}
Loading... ⏳ Please wait...

Controls

setNewFileName(e.target.value)} onKeyPress={e => e.key === 'Enter' && createFile()} onFocus={e => e.target.style.borderColor = theme.inputFocusBorder} onBlur={e => e.target.style.borderColor = theme.inputBorder} /> {(() => { const { ref, style, onMouseEnter, onMouseLeave } = iconButtonStyler(theme.panelBg, theme.border, theme.primary); return ; })()}
setNewFolderName(e.target.value)} onKeyPress={e => e.key === 'Enter' && createFolder()} onFocus={e => e.target.style.borderColor = theme.inputFocusBorder} onBlur={e => e.target.style.borderColor = theme.inputBorder} /> {(() => { const { ref, style, onMouseEnter, onMouseLeave } = iconButtonStyler(theme.panelBg, theme.border, theme.primary); return ; })()}
{(() => { const { ref, style, onMouseEnter, onMouseLeave } = createButtonStyler(theme.primary, theme.primaryHover, theme.primaryText); return ; })()}

File System

{renderFiles(files())}
{secondaryButtonProps('Download All (Root) as ZIP 📦', handleDownloadAll)}
Select a file to embark on an editing adventure! 🚀
: null }>

Now Editing: {selectedFile()}

editorRef = el} style={codeMirrorEditorStyle}>
{primaryButtonProps('💾 Save', saveFile)} {destructiveButtonProps('❌ Close', closeFileEditor)}
(window.innerWidth - 200) ? 'translateX(-100%)' : 'none' }}> {[ { label: '✏️ Rename', action: handleRenameClick, show: () => true }, { label: '📂 Open', action: handleOpenFolderFromContextMenu, show: () => contextMenu().file?.isDir }, { label: '➕📄 Add New File', action: () => handleShowCreateItemModal(false), show: () => contextMenu().file?.isDir || !contextMenu().file }, { label: '➕📁 Add New Folder', action: () => handleShowCreateItemModal(true), show: () => contextMenu().file?.isDir || !contextMenu().file }, { label: '⬇️ Download', action: handleDownloadSelected, show: () => contextMenu().file && !contextMenu().file.isDir }, { label: '🌀 Unzip Here', action: handleUnzipSelected, show: () => { const file = contextMenu().file; return file && !file.isDir && file.name.toLowerCase().endsWith('.zip'); } }, { label: '🗑️ Delete', action: handleDeleteSelected, show: () => contextMenu().file, color: theme.destructive }, ].map(item => (
e.target.style.backgroundColor = theme.itemHoverBg} onMouseLeave={e => e.target.style.backgroundColor = 'transparent'} > {item.label}
))}
{contextMenu().file.name} ({contextMenu().file.isDir ? 'Folder' : 'File'})
e.stopPropagation()}>

Rename "{renameContainerInfo().file?.name}"

renameInputRef = el} style={modalInputStyle} value={renameContainerInfo().newName} onInput={e => setRenameContainerInfo(prev => ({ ...prev, newName: e.target.value }))} onKeyPress={e => e.key === 'Enter' && performRename()} onFocus={e => e.target.style.borderColor = theme.inputFocusBorder} onBlur={e => e.target.style.borderColor = theme.inputBorder} />
{primaryButtonProps('✔️ Confirm', performRename)} {destructiveButtonProps('✖️ Cancel', cancelRename)}
e.stopPropagation()}>

Create New {createItemModalInfo().isDir ? 'Folder' : 'File'} in "{createItemModalInfo().parentPath || 'root'}"

createItemInputRef = el} style={modalInputStyle} placeholder={createItemModalInfo().isDir ? 'New folder name...' : 'New file name (e.g., script.js)'} value={createItemModalInfo().itemName} onInput={e => setCreateItemModalInfo(prev => ({ ...prev, itemName: e.target.value }))} onKeyPress={e => e.key === 'Enter' && performCreateItem()} onFocus={e => e.target.style.borderColor = theme.inputFocusBorder} onBlur={e => e.target.style.borderColor = theme.inputBorder} />
{primaryButtonProps('✔️ Create', performCreateItem)} {destructiveButtonProps('✖️ Cancel', cancelCreateItem)}
); } render(() => , document.getElementById('app'));