feat: add companion API and Android APK shell
This commit is contained in:
@@ -1,10 +1,118 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { FileEntry, FileReadResponse } from '@hermes-mobile/shared';
|
||||
import { useCompanion } from '../../api/CompanionContext.js';
|
||||
|
||||
function parentPath(path: string): string {
|
||||
if (path === '.') return '.';
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
parts.pop();
|
||||
return parts.length ? parts.join('/') : '.';
|
||||
}
|
||||
|
||||
export function FilesScreen() {
|
||||
const { client } = useCompanion();
|
||||
const [path, setPath] = useState('.');
|
||||
const [entries, setEntries] = useState<FileEntry[]>([]);
|
||||
const [activeFile, setActiveFile] = useState<FileReadResponse | null>(null);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function loadDirectory(nextPath = path): Promise<void> {
|
||||
setBusy(true);
|
||||
setStatus('');
|
||||
|
||||
try {
|
||||
const response = await client.listFiles(nextPath);
|
||||
setPath(response.path);
|
||||
setEntries(response.entries);
|
||||
setActiveFile(null);
|
||||
setDraft('');
|
||||
} catch (caughtError) {
|
||||
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to list files');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function openEntry(entry: FileEntry): Promise<void> {
|
||||
if (entry.type === 'directory') {
|
||||
await loadDirectory(entry.path);
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setStatus('');
|
||||
|
||||
try {
|
||||
const response = await client.readFile(entry.path);
|
||||
setActiveFile(response);
|
||||
setDraft(response.content);
|
||||
} catch (caughtError) {
|
||||
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to read file');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile(): Promise<void> {
|
||||
if (!activeFile) return;
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
const response = await client.writeFile(activeFile.path, draft);
|
||||
setStatus(`Saved ${response.bytesWritten} bytes to ${response.path}`);
|
||||
await openEntry({ name: activeFile.path, path: activeFile.path, type: 'file', size: 0, modifiedAt: new Date().toISOString() });
|
||||
} catch (caughtError) {
|
||||
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to save file');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadDirectory('.');
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<section className="screen stack-screen">
|
||||
<h2>Files</h2>
|
||||
<article className="empty-card">
|
||||
<p>Uploads and generated artifacts will live here for reuse, download, and sharing.</p>
|
||||
<div className="screen-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Workspace</p>
|
||||
<h2>Files</h2>
|
||||
</div>
|
||||
<button className="small-button" onClick={() => void loadDirectory()} type="button">Refresh</button>
|
||||
</div>
|
||||
|
||||
<article className="panel-card">
|
||||
<div className="path-bar">
|
||||
<button disabled={path === '.' || busy} onClick={() => void loadDirectory(parentPath(path))} type="button">Up</button>
|
||||
<span>{path}</span>
|
||||
</div>
|
||||
<div className="file-list">
|
||||
{entries.map((entry) => (
|
||||
<button className="file-row" key={entry.path} onClick={() => void openEntry(entry)} type="button">
|
||||
<span>{entry.type === 'directory' ? '📁' : '📄'} {entry.name}</span>
|
||||
<small>{entry.type === 'file' ? `${entry.size} B` : 'dir'}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{activeFile ? (
|
||||
<article className="panel-card editor-card">
|
||||
<div className="screen-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Editing</p>
|
||||
<h3>{activeFile.path}</h3>
|
||||
</div>
|
||||
<button className="small-button" disabled={busy} onClick={() => void saveFile()} type="button">Save</button>
|
||||
</div>
|
||||
<textarea aria-label="File content" onChange={(event) => setDraft(event.target.value)} value={draft} />
|
||||
</article>
|
||||
) : null}
|
||||
|
||||
{status ? <p className="error-card">{status}</p> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user