| |
|
|
| |
| let currentProject = null; |
| let currentFile = null; |
| let currentFilePath = null; |
| let terminal = null; |
| let ws = null; |
| let activeBuildId = null; |
| let files = []; |
| let projects = []; |
|
|
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| lucide.createIcons(); |
| |
| |
| initTerminal(); |
| |
| |
| loadProjects(); |
| |
| |
| setupEventListeners(); |
| |
| |
| updateStatus('Ready', 'success'); |
| }); |
|
|
| |
| function initTerminal() { |
| const terminalContainer = document.getElementById('terminal-panel'); |
| |
| terminal = new Terminal({ |
| theme: { |
| background: '#1E1E1E', |
| foreground: '#A9B7C6', |
| cursor: '#3DDC84', |
| selection: '#214283' |
| }, |
| fontFamily: 'JetBrains Mono, monospace', |
| fontSize: 12, |
| cursorBlink: true, |
| scrollback: 5000 |
| }); |
| |
| terminal.open(terminalContainer); |
| |
| |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const wsUrl = `${protocol}//${window.location.host}`; |
| |
| ws = new WebSocket(wsUrl); |
| |
| ws.onopen = () => { |
| terminal.write('\x1b[32mAndroid Studio Web Terminal\x1b[0m\r\n'); |
| terminal.write('\x1b[32mProject workspace: /workspace\x1b[0m\r\n'); |
| terminal.write('$ '); |
| }; |
| |
| ws.onmessage = (event) => { |
| const data = JSON.parse(event.data); |
| if (data.type === 'data') { |
| terminal.write(data.data); |
| } |
| }; |
| |
| ws.onerror = (error) => { |
| terminal.write('\x1b[31mTerminal connection error\x1b[0m\r\n'); |
| }; |
| |
| terminal.onData((data) => { |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify({ type: 'input', data })); |
| } |
| }); |
| |
| terminal.onResize((size) => { |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify({ type: 'resize', data: size })); |
| } |
| }); |
| } |
|
|
| |
| async function loadProjects() { |
| try { |
| const response = await fetch('/api/projects'); |
| const data = await response.json(); |
| projects = data.projects || []; |
| renderProjectList(); |
| } catch (error) { |
| console.error('Failed to load projects:', error); |
| } |
| } |
|
|
| function renderProjectList() { |
| const projectList = document.getElementById('project-list'); |
| if (!projectList) return; |
| |
| if (projects.length === 0) { |
| projectList.innerHTML = '<div class="p-2 text-gray-400">No projects found</div>'; |
| return; |
| } |
| |
| let html = ''; |
| projects.forEach(project => { |
| html += ` |
| <div class="project-item" onclick="openProject('${project}')"> |
| <i data-lucide="folder" class="w-4 h-4 text-yellow-500"></i> |
| <span>${project}</span> |
| </div> |
| `; |
| }); |
| |
| projectList.innerHTML = html; |
| lucide.createIcons(); |
| } |
|
|
| function toggleProjectDropdown() { |
| const dropdown = document.getElementById('project-dropdown'); |
| dropdown.classList.toggle('hidden'); |
| } |
|
|
| async function createNewProject() { |
| const name = prompt('Enter project name:'); |
| if (!name) return; |
| |
| updateStatus('Creating project...', 'building'); |
| |
| try { |
| const response = await fetch('/api/project/create', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ name }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.success) { |
| await loadProjects(); |
| await openProject(name); |
| updateStatus('Project created', 'success'); |
| } else { |
| updateStatus('Failed to create project', 'error'); |
| } |
| } catch (error) { |
| updateStatus('Error creating project', 'error'); |
| } |
| |
| toggleProjectDropdown(); |
| } |
|
|
| async function openProject(projectName) { |
| currentProject = projectName; |
| document.getElementById('current-project').textContent = projectName; |
| document.getElementById('project-name-display').textContent = projectName; |
| document.getElementById('preview-app-name').textContent = projectName; |
| |
| toggleProjectDropdown(); |
| await loadProjectFiles(); |
| } |
|
|
| async function loadProjectFiles() { |
| if (!currentProject) return; |
| |
| try { |
| const response = await fetch(`/api/files/${currentProject}/`); |
| const data = await response.json(); |
| |
| if (data.type === 'directory') { |
| files = data.files || []; |
| renderFileTree(); |
| } |
| } catch (error) { |
| console.error('Failed to load files:', error); |
| } |
| } |
|
|
| function renderFileTree() { |
| const treeElement = document.getElementById('file-tree'); |
| |
| if (files.length === 0) { |
| treeElement.innerHTML = '<div class="px-3 py-1 text-xs text-gray-500">No files found</div>'; |
| return; |
| } |
| |
| let html = '<div class="px-3 py-1 text-xs text-gray-500 uppercase">app/src/main</div>'; |
| |
| |
| files.forEach(file => { |
| if (file.includes('.')) { |
| const icon = file.endsWith('.kt') ? 'file-code' : |
| file.endsWith('.xml') ? 'file-text' : 'file'; |
| const color = file.endsWith('.kt') ? 'text-blue-300' : |
| file.endsWith('.xml') ? 'text-orange-300' : 'text-gray-400'; |
| |
| html += ` |
| <div class="file-item px-3 py-1.5 flex items-center space-x-2 cursor-pointer text-sm" |
| onclick="openFile('${file}')"> |
| <i data-lucide="${icon}" class="w-4 h-4 ${color}"></i> |
| <span>${file}</span> |
| </div> |
| `; |
| } |
| }); |
| |
| treeElement.innerHTML = html; |
| lucide.createIcons(); |
| } |
|
|
| |
| async function openFile(filename) { |
| if (!currentProject) { |
| alert('Please open a project first'); |
| return; |
| } |
| |
| currentFile = filename; |
| currentFilePath = filename; |
| |
| try { |
| const response = await fetch(`/api/files/${currentProject}/${filename}`); |
| const data = await response.json(); |
| |
| if (data.type === 'file') { |
| document.getElementById('code-editor').innerHTML = data.content; |
| updateLineNumbers(); |
| addTab(filename); |
| } |
| } catch (error) { |
| console.error('Failed to open file:', error); |
| } |
| |
| |
| document.querySelectorAll('.file-item').forEach(el => { |
| el.classList.remove('active'); |
| if (el.textContent.trim() === filename) { |
| el.classList.add('active'); |
| } |
| }); |
| } |
|
|
| function addTab(filename) { |
| const tabs = document.getElementById('editor-tabs'); |
| |
| |
| const existingTab = Array.from(tabs.children).find( |
| tab => tab.dataset.file === filename |
| ); |
| |
| if (existingTab) return; |
| |
| const icon = filename.endsWith('.kt') ? 'file-code' : |
| filename.endsWith('.xml') ? 'file-text' : 'file'; |
| const color = filename.endsWith('.kt') ? 'text-blue-300' : |
| filename.endsWith('.xml') ? 'text-orange-300' : 'text-gray-400'; |
| |
| const tabHtml = ` |
| <div class="px-4 py-2 flex items-center space-x-2 text-sm text-white bg-[#2B2B2B] border-t-2 border-[#3DDC84] cursor-pointer" |
| data-file="${filename}" |
| onclick="switchToTab('${filename}')"> |
| <i data-lucide="${icon}" class="w-4 h-4 ${color}"></i> |
| <span>${filename}</span> |
| <i data-lucide="x" class="w-3 h-3 hover:text-red-400 cursor-pointer ml-2" |
| onclick="closeTab(event, '${filename}')"></i> |
| </div> |
| `; |
| |
| tabs.insertAdjacentHTML('beforeend', tabHtml); |
| lucide.createIcons(); |
| } |
|
|
| function switchToTab(filename) { |
| openFile(filename); |
| } |
|
|
| function closeTab(event, filename) { |
| event.stopPropagation(); |
| const tab = event.target.closest('[data-file]'); |
| if (tab) { |
| tab.remove(); |
| if (currentFile === filename) { |
| const firstTab = document.querySelector('[data-file]'); |
| if (firstTab) { |
| openFile(firstTab.dataset.file); |
| } else { |
| currentFile = null; |
| document.getElementById('code-editor').innerHTML = '// No file open'; |
| updateLineNumbers(); |
| } |
| } |
| } |
| } |
|
|
| async function saveCurrentFile() { |
| if (!currentProject || !currentFile) { |
| alert('No file open'); |
| return; |
| } |
| |
| const content = document.getElementById('code-editor').innerText; |
| |
| try { |
| const response = await fetch(`/api/file/${currentProject}/${currentFile}`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ content }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.success) { |
| updateStatus('File saved', 'success'); |
| } |
| } catch (error) { |
| updateStatus('Save failed', 'error'); |
| } |
| } |
|
|
| function createNewFile() { |
| if (!currentProject) { |
| alert('Please open a project first'); |
| return; |
| } |
| |
| const filename = prompt('Enter file name (e.g., NewFile.kt):'); |
| if (filename) { |
| currentFile = filename; |
| document.getElementById('code-editor').innerHTML = getDefaultContent(filename); |
| updateLineNumbers(); |
| addTab(filename); |
| saveCurrentFile(); |
| } |
| } |
|
|
| function getDefaultContent(filename) { |
| if (filename.endsWith('.kt')) { |
| return `package com.example.app |
| |
| import android.os.Bundle |
| import androidx.appcompat.app.AppCompatActivity |
| |
| class MainActivity : AppCompatActivity() { |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| setContentView(R.layout.activity_main) |
| } |
| }`; |
| } else if (filename.endsWith('.xml')) { |
| return `<?xml version="1.0" encoding="utf-8"?> |
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| android:layout_width="match_parent" |
| android:layout_height="match_parent" |
| android:orientation="vertical"> |
| |
| <TextView |
| android:layout_width="wrap_content" |
| android:layout_height="wrap_content" |
| android:text="Hello World!" /> |
| |
| </LinearLayout>`; |
| } |
| return ''; |
| } |
|
|
| function refreshFiles() { |
| if (currentProject) { |
| loadProjectFiles(); |
| } |
| } |
|
|
| |
| async function startBuild() { |
| if (!currentProject) { |
| alert('Please open a project first'); |
| return; |
| } |
| |
| switchPanel('build'); |
| const buildPanel = document.getElementById('build-panel'); |
| buildPanel.innerHTML = '<div class="building">🔨 Building APK...</div>'; |
| updateStatus('Building...', 'building'); |
| |
| try { |
| const response = await fetch(`/api/build/${currentProject}`, { |
| method: 'POST' |
| }); |
| |
| const data = await response.json(); |
| activeBuildId = data.buildId; |
| |
| |
| checkBuildStatus(activeBuildId); |
| |
| } catch (error) { |
| buildPanel.innerHTML = '<div class="build-error">❌ Build failed to start</div>'; |
| updateStatus('Build failed', 'error'); |
| } |
| } |
|
|
| async function checkBuildStatus(buildId) { |
| const buildPanel = document.getElementById('build-panel'); |
| |
| try { |
| const response = await fetch(`/api/build/${buildId}`); |
| const data = await response.json(); |
| |
| if (data.output) { |
| let html = data.output.replace(/\n/g, '<br>') |
| .replace(/SUCCESS/g, '<span class="build-success">SUCCESS</span>') |
| .replace(/FAILED/g, '<span class="build-error">FAILED</span>') |
| .replace(/warning/g, '<span class="build-warning">warning</span>'); |
| |
| buildPanel.innerHTML = html; |
| } |
| |
| if (data.success !== undefined) { |
| if (data.success) { |
| buildPanel.innerHTML += '<br><br><div class="build-success">✅ Build successful! APK ready.</div>'; |
| updateStatus('Build successful', 'success'); |
| } else { |
| buildPanel.innerHTML += '<br><br><div class="build-error">❌ Build failed</div>'; |
| updateStatus('Build failed', 'error'); |
| } |
| } else { |
| |
| setTimeout(() => checkBuildStatus(buildId), 1000); |
| } |
| |
| } catch (error) { |
| buildPanel.innerHTML = '<div class="build-error">Error checking build status</div>'; |
| } |
| } |
|
|
| async function downloadAPK() { |
| if (!currentProject) { |
| alert('Please open a project first'); |
| return; |
| } |
| |
| window.location.href = `/api/apk/${currentProject}`; |
| } |
|
|
| |
| async function uploadLogo() { |
| if (!currentProject) { |
| alert('Please open a project first'); |
| return; |
| } |
| |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.accept = 'image/*'; |
| |
| input.onchange = async (e) => { |
| const file = e.target.files[0]; |
| const formData = new FormData(); |
| formData.append('logo', file); |
| |
| updateStatus('Uploading logo...', 'building'); |
| |
| try { |
| const response = await fetch(`/api/logo/upload/${currentProject}`, { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.success) { |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| document.getElementById('preview-logo').src = e.target.result; |
| }; |
| reader.readAsDataURL(file); |
| |
| updateStatus('Logo updated', 'success'); |
| } |
| } catch (error) { |
| updateStatus('Logo upload failed', 'error'); |
| } |
| }; |
| |
| input.click(); |
| } |
|
|
| function installOnPreview() { |
| const appScreen = document.getElementById('app-screen'); |
| const logo = document.getElementById('preview-logo'); |
| const appName = document.getElementById('preview-app-name'); |
| |
| |
| updateStatus('Installing on Android 10...', 'building'); |
| |
| setTimeout(() => { |
| appScreen.innerHTML = ` |
| <div class="logo-container"> |
| <img id="preview-logo" src="${logo.src}" alt="App Logo" style="width: 96px; height: 96px; border-radius: 20px;"> |
| <h3 id="preview-app-name" style="font-size: 20px; font-weight: 600; color: #333; margin-top: 16px;">${appName.textContent}</h3> |
| <p style="font-size: 12px; color: #666; margin-top: 4px;">Running on Android 10</p> |
| <div style="margin-top: 20px; padding: 8px 16px; background: #3DDC84; color: white; border-radius: 20px; font-size: 12px;"> |
| App installed |
| </div> |
| </div> |
| `; |
| updateStatus('App installed on preview', 'success'); |
| }, 2000); |
| } |
|
|
| function rotatePreview() { |
| const phone = document.getElementById('phone-preview'); |
| phone.classList.toggle('rotated'); |
| } |
|
|
| function toggleTheme() { |
| const statusBar = document.getElementById('status-bar'); |
| const appScreen = document.getElementById('app-screen'); |
| const navBar = document.getElementById('nav-bar'); |
| |
| statusBar.classList.toggle('light'); |
| appScreen.classList.toggle('dark'); |
| navBar.classList.toggle('light'); |
| } |
|
|
| function simulateBack() { |
| updateStatus('Back button pressed', 'info'); |
| } |
|
|
| function simulateHome() { |
| updateStatus('Home button pressed', 'info'); |
| |
| const appScreen = document.getElementById('app-screen'); |
| appScreen.innerHTML = ` |
| <div class="logo-container"> |
| <img id="preview-logo" src="${document.getElementById('preview-logo').src}" alt="App Logo" style="width: 96px; height: 96px; border-radius: 20px;"> |
| <h3 id="preview-app-name" style="font-size: 20px; font-weight: 600; color: #333; margin-top: 16px;">${document.getElementById('preview-app-name').textContent}</h3> |
| <p class="app-version">Android 10 (API 29)</p> |
| </div> |
| `; |
| } |
|
|
| function simulateRecent() { |
| updateStatus('Recent apps button pressed', 'info'); |
| } |
|
|
| |
| function updateLineNumbers() { |
| const editor = document.getElementById('code-editor'); |
| const lines = editor.innerText.split('\n').length; |
| const lineNumbers = document.getElementById('line-numbers'); |
| |
| let html = ''; |
| for (let i = 1; i <= lines; i++) { |
| html += `<div class="leading-6">${i}</div>`; |
| } |
| lineNumbers.innerHTML = html; |
| } |
|
|
| function syncScroll() { |
| const editor = document.getElementById('code-editor'); |
| const lineNumbers = document.getElementById('line-numbers'); |
| lineNumbers.scrollTop = editor.scrollTop; |
| } |
|
|
| function switchPanel(panel) { |
| |
| document.getElementById('terminal-panel').classList.add('hidden'); |
| document.getElementById('build-panel').classList.add('hidden'); |
| |
| |
| document.getElementById(`${panel}-panel`).classList.remove('hidden'); |
| |
| |
| document.querySelectorAll('.panel-tab').forEach(tab => { |
| tab.classList.remove('border-[#3DDC84]', 'text-white'); |
| tab.classList.add('text-gray-400'); |
| }); |
| |
| event.currentTarget.classList.add('border-[#3DDC84]', 'text-white'); |
| event.currentTarget.classList.remove('text-gray-400'); |
| } |
|
|
| function updateStatus(message, type) { |
| const statusEl = document.getElementById('status-message'); |
| statusEl.textContent = message; |
| |
| const colors = { |
| success: '#3DDC84', |
| error: '#CF6679', |
| building: '#FBC02D', |
| info: '#64B5F6' |
| }; |
| |
| statusEl.style.color = colors[type] || '#000000'; |
| } |
|
|
| function openSettings() { |
| alert('Settings dialog would open here'); |
| } |
|
|
| function setupEventListeners() { |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.ctrlKey && e.key === 's') { |
| e.preventDefault(); |
| saveCurrentFile(); |
| } |
| |
| if (e.key === 'F5') { |
| e.preventDefault(); |
| startBuild(); |
| } |
| }); |
| |
| |
| document.addEventListener('click', (e) => { |
| if (!e.target.closest('.relative')) { |
| document.getElementById('project-dropdown').classList.add('hidden'); |
| } |
| }); |
| } |
|
|
| |
| window.toggleProjectDropdown = toggleProjectDropdown; |
| window.createNewProject = createNewProject; |
| window.openProject = openProject; |
| window.openFile = openFile; |
| window.saveCurrentFile = saveCurrentFile; |
| window.createNewFile = createNewFile; |
| window.refreshFiles = refreshFiles; |
| window.startBuild = startBuild; |
| window.downloadAPK = downloadAPK; |
| window.installOnPreview = installOnPreview; |
| window.rotatePreview = rotatePreview; |
| window.toggleTheme = toggleTheme; |
| window.uploadLogo = uploadLogo; |
| window.simulateBack = simulateBack; |
| window.simulateHome = simulateHome; |
| window.simulateRecent = simulateRecent; |
| window.switchPanel = switchPanel; |
| window.updateLineNumbers = updateLineNumbers; |
| window.syncScroll = syncScroll; |
| window.openSettings = openSettings; |
| window.switchToTab = switchToTab; |
| window.closeTab = closeTab; |