/** * FilesTile β€” Datei-Browser eines Projekts (/shared/projects//). * * 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 = ({ projectId, focused }) => { const [files, setFiles] = useState([]); 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 ( πŸ“ Dateien ); } return ( Dateien{files.length ? ` (${files.length})` : ''} ↻ {loading && files.length === 0 ? ( ) : err ? ( {err} ) : files.length === 0 ? ( {!projectId ? 'Kein aktives Projekt β€” wechsle in ein Projekt fΓΌr dessen Dateien.' : 'Noch keine Dateien in diesem Projekt.'} ) : ( files.map(f => ( open(f)} style={styles.row} disabled={previewBusy === f.path}> {iconFor(f.path)} {f.path} {previewBusy === f.path ? : {humanSize(f.size)}} )) )} setPreview(null)}> {preview?.path} setPreview(null)}>βœ• {preview?.kind === 'image' ? ( ) : ( {preview?.data} )} ); }; 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;