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 (