Reuben_OS / app /components /FlutterRunner.tsx
Reubencf's picture
Fix file loading to prevent overwriting clicked file
5d16c42
'use client'
import React, { useState, useEffect } from 'react'
import Editor from '@monaco-editor/react'
import {
Download,
SidebarSimple,
FileCode,
CaretDown,
FloppyDisk
} from '@phosphor-icons/react'
import Window from './Window'
interface FlutterRunnerProps {
onClose: () => void
onMinimize?: () => void
onMaximize?: () => void
onFocus?: () => void
zIndex?: number
initialCode?: string
}
interface FileNode {
id: string
name: string
type: 'file' | 'folder'
content?: string
children?: FileNode[]
isOpen?: boolean
}
const DEFAULT_FLUTTER_CODE = ``
export function FlutterRunner({ onClose, onMinimize, onMaximize, onFocus, zIndex, initialCode }: FlutterRunnerProps) {
const [code, setCode] = useState(initialCode || DEFAULT_FLUTTER_CODE)
const [showEditor, setShowEditor] = useState(true)
const [showFiles, setShowFiles] = useState(true)
const [files, setFiles] = useState<FileNode[]>([])
const [activeFileId, setActiveFileId] = useState('main')
const [activeFileName, setActiveFileName] = useState('main.dart')
const [lastSaved, setLastSaved] = useState<Date | null>(null)
const [passkey, setPasskey] = useState('')
const [loading, setLoading] = useState(false)
// Load all files for the file tree without changing the active file
const loadAllFilesForTree = async (key: string, currentFileName: string) => {
try {
const response = await fetch(`/api/data?key=${encodeURIComponent(key)}&folder=`)
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
// Filter for .dart files
const dartFiles = data.files?.filter((f: any) => f.name.endsWith('.dart')) || []
if (dartFiles.length > 0) {
const fileNodes: FileNode[] = dartFiles.map((file: any, index: number) => ({
id: `file_${index}`,
name: file.name,
type: 'file' as const,
content: file.content
}))
setFiles([{
id: 'root',
name: 'Dart Files',
type: 'folder',
isOpen: true,
children: fileNodes
}])
// Set active file ID to match the current file
const currentFileNode = fileNodes.find(f => f.name === currentFileName)
if (currentFileNode) {
setActiveFileId(currentFileNode.id)
}
} else {
// No files found, just show the current file
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: currentFileName, type: 'file', content: '' }
]
}])
}
} catch (err) {
console.error('Error loading file tree:', err)
// On error, just show the current file
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: currentFileName, type: 'file', content: '' }
]
}])
}
}
// Load files from secure storage (used when no specific file is selected)
const loadFiles = async (key: string) => {
setLoading(true)
try {
const response = await fetch(`/api/data?key=${encodeURIComponent(key)}&folder=`)
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
// Filter for .dart files
const dartFiles = data.files?.filter((f: any) => f.name.endsWith('.dart')) || []
if (dartFiles.length > 0) {
const fileNodes: FileNode[] = dartFiles.map((file: any, index: number) => ({
id: `file_${index}`,
name: file.name,
type: 'file' as const,
content: file.content
}))
setFiles([{
id: 'root',
name: 'Dart Files',
type: 'folder',
isOpen: true,
children: fileNodes
}])
// Load first file
if (fileNodes.length > 0) {
setActiveFileId(fileNodes[0].id)
setActiveFileName(fileNodes[0].name)
setCode(fileNodes[0].content || DEFAULT_FLUTTER_CODE)
}
} else {
// No files found, create default
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: 'main.dart', type: 'file', content: DEFAULT_FLUTTER_CODE }
]
}])
setCode(DEFAULT_FLUTTER_CODE)
setActiveFileName('main.dart')
}
} catch (err) {
console.error('Error loading files:', err)
// Create default on error
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: 'main.dart', type: 'file', content: DEFAULT_FLUTTER_CODE }
]
}])
setCode(DEFAULT_FLUTTER_CODE)
setActiveFileName('main.dart')
} finally {
setLoading(false)
}
}
const handleFileClick = (file: FileNode) => {
if (file.type === 'file') {
setActiveFileId(file.id)
setActiveFileName(file.name)
setCode(file.content || '')
}
}
// Check for initial passkey and file content on mount
useEffect(() => {
const sessionPasskey = sessionStorage.getItem('currentPasskey')
const sessionFileContent = sessionStorage.getItem('flutterFileContent')
const sessionFileName = sessionStorage.getItem('currentFileName')
if (sessionFileContent) {
// Load file from session storage (from FileManager)
console.log('Loading dart file from session:', sessionFileName, 'Content length:', sessionFileContent.length)
setCode(sessionFileContent)
setActiveFileName(sessionFileName || 'main.dart')
if (sessionPasskey) {
setPasskey(sessionPasskey)
// Don't call loadFiles here - it would overwrite the file we just loaded
// Just load all files for the file tree without changing the active file
loadAllFilesForTree(sessionPasskey, sessionFileName || 'main.dart')
} else {
// For public files, just set up a simple file structure
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: sessionFileName || 'main.dart', type: 'file', content: sessionFileContent }
]
}])
}
// Clear session storage after loading
sessionStorage.removeItem('flutterFileContent')
} else if (sessionPasskey) {
setPasskey(sessionPasskey)
loadFiles(sessionPasskey)
} else if (initialCode) {
setCode(initialCode)
} else {
// Initialize with default empty file
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: 'main.dart', type: 'file', content: DEFAULT_FLUTTER_CODE }
]
}])
}
}, [initialCode])
// Listen for new files being loaded while the component is already open
useEffect(() => {
const checkForNewFile = () => {
const sessionFileContent = sessionStorage.getItem('flutterFileContent')
const sessionFileName = sessionStorage.getItem('currentFileName')
const sessionPasskey = sessionStorage.getItem('currentPasskey')
if (sessionFileContent) {
console.log('Detected new file in session storage:', sessionFileName)
setCode(sessionFileContent)
setActiveFileName(sessionFileName || 'main.dart')
if (sessionPasskey) {
setPasskey(sessionPasskey)
// Load all files for the file tree without overwriting the current file
loadAllFilesForTree(sessionPasskey, sessionFileName || 'main.dart')
} else {
// For public files, just set up a simple file structure
setFiles([{
id: 'root',
name: 'lib',
type: 'folder',
isOpen: true,
children: [
{ id: 'main', name: sessionFileName || 'main.dart', type: 'file', content: sessionFileContent }
]
}])
}
// Clear session storage after loading
sessionStorage.removeItem('flutterFileContent')
}
}
// Check every 500ms for new files
const interval = setInterval(checkForNewFile, 500)
return () => clearInterval(interval)
}, [])
// Manual save function
const handleSave = async () => {
if (!passkey || !activeFileName) {
alert('Cannot save: No passkey set. This file is from public storage.')
return
}
try {
setLoading(true)
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: passkey,
passkey: passkey,
action: 'save_file',
fileName: activeFileName,
content: code
})
})
setLastSaved(new Date())
} catch (error) {
console.error('Save failed:', error)
alert('Failed to save file')
} finally {
setLoading(false)
}
}
const handleDownload = () => {
const blob = new Blob([code], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = activeFileName || 'main.dart'
a.click()
URL.revokeObjectURL(url)
}
return (
<Window
id="flutter-runner"
title="Flutter IDE"
isOpen={true}
onClose={onClose}
onMinimize={onMinimize}
onMaximize={onMaximize}
onFocus={onFocus}
zIndex={zIndex}
width={1400}
height={800}
x={40}
y={40}
className="flutter-ide-window"
>
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
{/* Code Editor - Conditionally Rendered */}
{showEditor && (
<div className="w-[600px] flex flex-col border-r border-[#333]">
{/* Toolbar */}
<div className="h-10 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<button
onClick={() => setShowFiles(!showFiles)}
className={`p-1.5 rounded hover:bg-[#3e3e42] ${showFiles ? 'text-white' : 'text-gray-500'}`}
title="Toggle Files"
>
<SidebarSimple size={16} />
</button>
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
<FileCode size={14} className="text-blue-400" />
{activeFileName}
</div>
{lastSaved && (
<span className="text-xs text-green-400">
✓ Saved {lastSaved.toLocaleTimeString()}
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={!passkey || loading}
className={`p-1.5 rounded transition-colors ${
passkey && !loading
? 'text-blue-400 hover:text-blue-300 hover:bg-[#3e3e42]'
: 'text-gray-600 cursor-not-allowed'
}`}
title={passkey ? "Save to Secure Storage" : "Cannot save public files"}
>
<FloppyDisk size={16} />
</button>
<button
onClick={handleDownload}
className="p-1.5 text-gray-400 hover:text-white hover:bg-[#3e3e42] rounded transition-colors"
title="Download Code"
>
<Download size={16} />
</button>
</div>
</div>
{/* Monaco Editor */}
<div className="flex-1">
<Editor
height="100%"
defaultLanguage="dart"
theme="vs-dark"
value={code}
onChange={(value) => setCode(value || '')}
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 16, bottom: 16 },
renderLineHighlight: 'all',
smoothScrolling: true,
cursorBlinking: 'smooth',
cursorSmoothCaretAnimation: 'on'
}}
/>
</div>
</div>
)}
{/* DartPad Preview - Takes full width when editor is hidden */}
<div className="flex-1 flex flex-col">
<div className="h-10 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
<button
onClick={() => setShowEditor(!showEditor)}
className={`flex items-center gap-2 px-2 py-1 rounded transition-all ${showEditor
? 'bg-[#3e3e42] text-white hover:bg-[#4e4e52]'
: 'bg-blue-600 text-white hover:bg-blue-700'}`}
title={showEditor ? "Hide Code Editor" : "Show Code Editor"}
>
<SidebarSimple size={16} weight={showEditor ? "fill" : "regular"} />
<span className="text-xs font-medium">
{showEditor ? "Hide Editor" : "Show Editor"}
</span>
</button>
<span className="text-xs text-gray-400 uppercase font-bold">
Live Preview {!showEditor && "- Full Screen"}
</span>
<div className="w-24" /> {/* Spacer for centering */}
</div>
<div className="flex-1 bg-[#1e1e1e] relative overflow-hidden">
<iframe
src="https://dartpad.dev/embed-flutter.html?theme=dark&split=50"
className="w-full h-full border-0"
sandbox="allow-scripts allow-same-origin allow-popups"
title="Flutter Preview"
/>
</div>
</div>
{/* File List - Right Side - Only show when editor is visible */}
{showEditor && showFiles && (
<div className="w-64 bg-[#252526] border-l border-[#333] flex flex-col">
<div className="h-9 px-4 flex items-center text-xs font-bold text-gray-500 uppercase tracking-wider">
Project Files
</div>
<div className="flex-1 overflow-y-auto py-2">
<div className="px-2">
<div className="flex items-center gap-1 py-1 px-2 text-sm text-gray-300 hover:bg-[#2a2d2e] rounded cursor-pointer">
<CaretDown size={12} weight="bold" />
<span className="font-bold">Flutter App</span>
</div>
<div className="pl-4">
{files.map(file => (
<div key={file.id}>
<div className="flex items-center gap-2 py-1 px-2 text-sm hover:bg-[#2a2d2e] rounded cursor-pointer text-blue-400">
<CaretDown size={12} weight="bold" />
{file.name}
</div>
{file.children?.map(child => (
<div
key={child.id}
onClick={() => handleFileClick(child)}
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
}`}
>
<FileCode size={14} />
{child.name}
</div>
))}
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
</Window>
)
}