Files
ARIA-AGENT/android/src/workspace/tiles/FilesTile.tsx
T
duffyduckandClaude Opus 4.8 933836f0a6 fix(app): kein 404 bei Dateien/Desktop im Hauptchat (leere project_id)
Im Hauptchat (projectId='') riefen FilesTile/DesktopTile /projects//files bzw.
/vms auf → Brain 404. Beide guarden jetzt leere projectId: kein Fetch, klare
Meldung "Kein aktives Projekt — wechsle in ein Projekt".

tsc clean. App-only, Deploy: APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:59:50 +02:00

150 lines
6.7 KiB
TypeScript

/**
* FilesTile — Datei-Browser eines Projekts (/shared/projects/<id>/).
*
* Listet ALLE Dateien (nicht nur Code): erzeugte Bilder, Logs, Assets … — die
* gleichen, die in der Projektliste als 📄 gezaehlt werden. Tippen auf ein Bild
* zeigt es; tippen auf eine Textdatei zeigt eine Vorschau.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, Image, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import brainApi from '../../services/brainApi';
interface Props {
projectId: string;
focused: boolean;
}
interface FileEntry { path: string; size: number }
const IMG_EXT = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'];
function ext(path: string): string { return (path.split('.').pop() || '').toLowerCase(); }
function isImage(path: string): boolean { return IMG_EXT.includes(ext(path)); }
function iconFor(path: string): string {
const e = ext(path);
if (isImage(path)) return '🖼️';
if (['md', 'txt', 'readme'].includes(e)) return '📄';
if (['asm', 's', 'c', 'h', 'cpp', 'py', 'js', 'ts', 'sh', 'go', 'rs'].includes(e)) return '📝';
if (['zip', 'tar', 'gz', 'img', 'iso', 'qcow2'].includes(e)) return '📦';
return '📄';
}
function humanSize(n: number): string {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / 1024 / 1024).toFixed(1)} MB`;
}
const FilesTile: React.FC<Props> = ({ projectId, focused }) => {
const [files, setFiles] = useState<FileEntry[]>([]);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState('');
const [preview, setPreview] = useState<{ path: string; kind: 'image' | 'text'; data: string } | null>(null);
const [previewBusy, setPreviewBusy] = useState('');
const load = useCallback(() => {
if (!projectId) { setFiles([]); setErr(''); setLoading(false); return; }
setLoading(true); setErr('');
brainApi.listProjectFiles(projectId)
.then(r => setFiles((r.files || []).slice().sort((a, b) => a.path.localeCompare(b.path))))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
}, [projectId]);
useEffect(() => { if (focused) load(); }, [focused, projectId, load]);
const open = useCallback((f: FileEntry) => {
setPreviewBusy(f.path); setErr('');
if (isImage(f.path)) {
brainApi.readProjectFileBinary(projectId, f.path)
.then(r => setPreview({ path: f.path, kind: 'image', data: `data:${r.mime};base64,${r.base64}` }))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setPreviewBusy(''));
} else {
brainApi.readProjectFile(projectId, f.path)
.then(r => setPreview({ path: f.path, kind: 'text', data: r.content ?? '' }))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setPreviewBusy(''));
}
}, [projectId]);
if (!focused) {
return (
<View style={styles.placeholder}>
<Text style={styles.icon}>📁</Text>
<Text style={styles.text}>Dateien</Text>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.bar}>
<Text style={styles.barTitle}>Dateien{files.length ? ` (${files.length})` : ''}</Text>
<TouchableOpacity onPress={load} style={styles.barBtn}><Text style={styles.barBtnText}></Text></TouchableOpacity>
</View>
<ScrollView contentContainerStyle={{ padding: 8 }}>
{loading && files.length === 0 ? (
<ActivityIndicator color="#0096FF" style={{ marginTop: 20 }} />
) : err ? (
<Text style={styles.err}>{err}</Text>
) : files.length === 0 ? (
<Text style={styles.empty}>{!projectId ? 'Kein aktives Projekt — wechsle in ein Projekt für dessen Dateien.' : 'Noch keine Dateien in diesem Projekt.'}</Text>
) : (
files.map(f => (
<TouchableOpacity key={f.path} onPress={() => open(f)} style={styles.row} disabled={previewBusy === f.path}>
<Text style={styles.rowIcon}>{iconFor(f.path)}</Text>
<Text style={styles.rowName} numberOfLines={1}>{f.path}</Text>
{previewBusy === f.path
? <ActivityIndicator color="#8888AA" size="small" />
: <Text style={styles.rowSize}>{humanSize(f.size)}</Text>}
</TouchableOpacity>
))
)}
</ScrollView>
<Modal visible={!!preview} transparent animationType="fade" onRequestClose={() => setPreview(null)}>
<View style={styles.pvOverlay}>
<View style={styles.pvBar}>
<Text style={styles.pvTitle} numberOfLines={1}>{preview?.path}</Text>
<TouchableOpacity onPress={() => setPreview(null)}><Text style={styles.pvClose}></Text></TouchableOpacity>
</View>
{preview?.kind === 'image' ? (
<Image source={{ uri: preview.data }} style={styles.pvImg} resizeMode="contain" />
) : (
<ScrollView style={styles.pvTextWrap} horizontal>
<ScrollView><Text style={styles.pvText}>{preview?.data}</Text></ScrollView>
</ScrollView>
)}
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0D0D1A' },
placeholder: { flex: 1, backgroundColor: '#0D0D1A', alignItems: 'center', justifyContent: 'center' },
icon: { fontSize: 56, marginBottom: 10 },
text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' },
bar: { height: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, backgroundColor: '#12122A', borderBottomColor: '#1E1E2E', borderBottomWidth: 1 },
barTitle: { color: '#E0E0F0', fontSize: 14, fontWeight: '700', flex: 1 },
barBtn: { paddingHorizontal: 10, paddingVertical: 4 },
barBtnText: { color: '#0096FF', fontSize: 14, fontWeight: '700' },
empty: { color: '#8888AA', fontSize: 13, textAlign: 'center', marginTop: 24 },
err: { color: '#FF6E6E', fontSize: 13, marginTop: 16, paddingHorizontal: 8 },
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 8, borderBottomColor: '#161628', borderBottomWidth: 1, gap: 10 },
rowIcon: { fontSize: 18 },
rowName: { color: '#E0E0F0', fontSize: 13, flex: 1 },
rowSize: { color: '#555570', fontSize: 11 },
pvOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.94)' },
pvBar: { flexDirection: 'row', alignItems: 'center', padding: 12, gap: 10 },
pvTitle: { color: '#E0E0F0', fontSize: 13, fontWeight: '700', flex: 1 },
pvClose: { color: '#E0E0F0', fontSize: 20, paddingHorizontal: 6 },
pvImg: { flex: 1, width: '100%' },
pvTextWrap: { flex: 1, padding: 12 },
pvText: { color: '#C8C8E0', fontSize: 12, fontFamily: 'monospace' },
});
export default FilesTile;