feat(app): Datei-Manager, Skill-Created-Bubble, Zoom rewriten, Repair-Cleanup
Drei groessere Aenderungen in der Android-App.
Datei-Manager (Settings → Dateien)
- Neuer Eintrag im Settings-Menue → Modal mit Liste
- Suche + Filter (Alle / Von ARIA / Vom User)
- Per Eintrag: ARIA/USER-Badge, Groesse, Datum, Loeschen-Button
- file_list_request via RVS → Bridge → Diagnostic-HTTP → response
- file_delete_request loescht serverseitig, file_deleted-Event
aktualisiert ALLE Chat-Bubbles (Attachment.deleted = true mit
Strikethrough-Name + 🗑️-Icon)
Skill-Created-Bubble
- Neuer ChatMessage.skillCreated Typ — eigenes Render mit gelbem
Border, Skill-Name, Beschreibung, Execution-Mode, Active-Status
- Falls Skill-Setup fehlschlug: ⚠ Setup-Fehler-Zeile direkt in der Bubble
- Stefan sieht in der Chat-History immer wenn ARIA selbst einen
Skill angelegt hat — Transparenz statt schweigend im Hintergrund
Pinch-Zoom rewriten (ZoomableImage.tsx)
- Multi-Touch-Race-Bugs in der alten Variante geloest:
* Touch-Count jetzt aus e.nativeEvent.touches.length statt
gestureState.numberActiveTouches (war nicht zuverlaessig)
* Re-Snapshot bei JEDEM Finger-Wechsel (1↔2) → keine Spruenge mehr
* Doppel-Tap via onPanResponderRelease + Bewegungs-Cap
* pointerEvents="none" auf Image-Wrapper → Touches gehen garantiert
an PanResponder-View
* collapsable={false} verhindert Android-View-Flattening
- 2-Finger-Pinch 1x..5x, simultaner Pan via Focal,
1-Finger-Pan nur wenn gezoomt (>1.02x), Doppel-Tap toggelt 1x↔2.5x
App SettingsScreen Repair-Section
- aria-core-spezifische Buttons raus: 🔧 Reparieren, 🚨 ARIA hart neu,
🧹 Konversation komprimieren (OpenClaw ist abgerissen)
- Stattdessen generischer container_restart fuer aria-bridge/brain/qdrant
- Repair-Buttons aus der "ARIA denkt..."-Bubble entfernt (nur Abbrechen)
ChatScreen
- skill_created und file_deleted Handler im RVS-Message-Switch
- file_list_response (Modal-State liegt in SettingsScreen)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,7 @@ const SETTINGS_SECTIONS = [
|
||||
{ id: 'wake_word', icon: '👂', label: 'Wake-Word', desc: 'Wake-Word-Auswahl' },
|
||||
{ id: 'voice_output', icon: '🔊', label: 'Sprachausgabe', desc: 'Stimmen, Pre-Roll, Geschwindigkeit' },
|
||||
{ id: 'storage', icon: '📁', label: 'Speicher', desc: 'Anhang-Speicherort, Auto-Download' },
|
||||
{ id: 'files', icon: '📂', label: 'Dateien', desc: 'ARIA- und User-Dateien — anzeigen, löschen' },
|
||||
{ id: 'protocol', icon: '📜', label: 'Protokoll', desc: 'Privatsphaere, Backup' },
|
||||
{ id: 'about', icon: 'ℹ️', label: 'Ueber', desc: 'App-Version, Update' },
|
||||
] as const;
|
||||
@@ -147,6 +148,13 @@ const SettingsScreen: React.FC = () => {
|
||||
const [xttsVoice, setXttsVoice] = useState('');
|
||||
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
|
||||
const [availableVoices, setAvailableVoices] = useState<Array<{name: string, size: number}>>([]);
|
||||
// Datei-Manager
|
||||
const [fileManagerOpen, setFileManagerOpen] = useState(false);
|
||||
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean}>>([]);
|
||||
const [fileManagerLoading, setFileManagerLoading] = useState(false);
|
||||
const [fileManagerError, setFileManagerError] = useState('');
|
||||
const [fileManagerSearch, setFileManagerSearch] = useState('');
|
||||
const [fileManagerFilter, setFileManagerFilter] = useState<'all' | 'aria' | 'user'>('all');
|
||||
const [voiceCloneVisible, setVoiceCloneVisible] = useState(false);
|
||||
const [tempPath, setTempPath] = useState('');
|
||||
// Sub-Screen Navigation: null = Hauptmenue, sonst eine der Section-IDs.
|
||||
@@ -371,6 +379,25 @@ const SettingsScreen: React.FC = () => {
|
||||
setAvailableVoices(voices);
|
||||
}
|
||||
|
||||
// Datei-Manager: Liste empfangen
|
||||
if (message.type === ('file_list_response' as any)) {
|
||||
const p: any = message.payload || {};
|
||||
if (p.ok) {
|
||||
setFileManagerFiles(p.files || []);
|
||||
} else {
|
||||
setFileManagerError(p.error || 'Unbekannter Fehler');
|
||||
}
|
||||
setFileManagerLoading(false);
|
||||
}
|
||||
|
||||
// Datei-Manager: Datei wurde geloescht (vom Diagnostic oder dieser App)
|
||||
if (message.type === ('file_deleted' as any)) {
|
||||
const p: any = message.payload || {};
|
||||
if (p.path) {
|
||||
setFileManagerFiles(prev => prev.filter(f => f.path !== p.path));
|
||||
}
|
||||
}
|
||||
|
||||
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
||||
if (message.type === ('xtts_voice_saved' as any)) {
|
||||
const name = (message.payload as any).name as string;
|
||||
@@ -564,6 +591,119 @@ const SettingsScreen: React.FC = () => {
|
||||
visible={voiceCloneVisible}
|
||||
onClose={() => setVoiceCloneVisible(false)}
|
||||
/>
|
||||
{/* Datei-Manager Modal */}
|
||||
<Modal
|
||||
visible={fileManagerOpen}
|
||||
animationType="slide"
|
||||
onRequestClose={() => setFileManagerOpen(false)}
|
||||
>
|
||||
<View style={{flex:1, backgroundColor:'#080810', paddingTop:24}}>
|
||||
<View style={{flexDirection:'row', alignItems:'center', padding:12, borderBottomWidth:1, borderColor:'#1E1E2E'}}>
|
||||
<TouchableOpacity onPress={() => setFileManagerOpen(false)} style={{padding:8}}>
|
||||
<Text style={{color:'#0096FF', fontSize:24}}>‹</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={{color:'#E0E0F0', fontSize:18, fontWeight:'600', flex:1, marginLeft:8}}>Dateien</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setFileManagerError('');
|
||||
setFileManagerLoading(true);
|
||||
rvs.send('file_list_request' as any, {});
|
||||
}}
|
||||
style={{padding:8}}
|
||||
>
|
||||
<Text style={{color:'#0096FF', fontSize:14}}>🔄</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={{padding:12}}>
|
||||
<TextInput
|
||||
style={{backgroundColor:'#1E1E2E', borderRadius:8, padding:10, color:'#E0E0F0', fontSize:14}}
|
||||
placeholder="Suche..."
|
||||
placeholderTextColor="#555570"
|
||||
value={fileManagerSearch}
|
||||
onChangeText={setFileManagerSearch}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<View style={{flexDirection:'row', marginTop:8, gap:6}}>
|
||||
{(['all','aria','user'] as const).map(f => (
|
||||
<TouchableOpacity
|
||||
key={f}
|
||||
onPress={() => setFileManagerFilter(f)}
|
||||
style={{
|
||||
paddingVertical:6, paddingHorizontal:12, borderRadius:14,
|
||||
backgroundColor: fileManagerFilter === f ? '#0096FF' : '#1E1E2E',
|
||||
}}
|
||||
>
|
||||
<Text style={{color: fileManagerFilter === f ? '#fff' : '#8888AA', fontSize:12}}>
|
||||
{f === 'all' ? 'Alle' : f === 'aria' ? 'Von ARIA' : 'Von dir'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
{fileManagerLoading ? (
|
||||
<Text style={{color:'#8888AA', textAlign:'center', marginTop:20}}>Lade...</Text>
|
||||
) : fileManagerError ? (
|
||||
<Text style={{color:'#FF6B6B', textAlign:'center', marginTop:20}}>{fileManagerError}</Text>
|
||||
) : (
|
||||
<ScrollView style={{flex:1}} contentContainerStyle={{padding:12}}>
|
||||
{(() => {
|
||||
let files = fileManagerFiles;
|
||||
if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria);
|
||||
else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria);
|
||||
if (fileManagerSearch) {
|
||||
const q = fileManagerSearch.toLowerCase();
|
||||
files = files.filter(f => f.name.toLowerCase().includes(q));
|
||||
}
|
||||
if (!files.length) {
|
||||
return <Text style={{color:'#555570', textAlign:'center', marginTop:20}}>Keine Dateien</Text>;
|
||||
}
|
||||
const fmtSize = (b: number) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`;
|
||||
return files.map(f => (
|
||||
<View key={f.path} style={{
|
||||
backgroundColor:'#0D0D1A', padding:12, borderRadius:8, marginBottom:8,
|
||||
flexDirection:'row', alignItems:'center', gap:8,
|
||||
}}>
|
||||
<View style={{flex:1}}>
|
||||
<View style={{flexDirection:'row', alignItems:'center'}}>
|
||||
<View style={{
|
||||
backgroundColor: f.fromAria ? '#0096FF22' : '#34C75922',
|
||||
paddingHorizontal:6, paddingVertical:1, borderRadius:3, marginRight:6,
|
||||
}}>
|
||||
<Text style={{color: f.fromAria ? '#0096FF' : '#34C759', fontSize:9}}>
|
||||
{f.fromAria ? 'ARIA' : 'USER'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={{color:'#E0E0F0', fontSize:13, flex:1}} numberOfLines={1}>{f.name}</Text>
|
||||
</View>
|
||||
<Text style={{color:'#555570', fontSize:10, marginTop:2}}>
|
||||
{fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'Datei löschen?',
|
||||
`"${f.name}"\n\nIn allen Chat-Bubbles wird sie als gelöscht markiert.`,
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Löschen', style: 'destructive', onPress: () => {
|
||||
rvs.send('file_delete_request' as any, { path: f.path });
|
||||
ToastAndroid.show('Lösch-Befehl gesendet…', ToastAndroid.SHORT);
|
||||
}},
|
||||
],
|
||||
);
|
||||
}}
|
||||
style={{padding:8}}
|
||||
>
|
||||
<Text style={{color:'#FF6B6B', fontSize:18}}>🗑</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
));
|
||||
})()}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
|
||||
{currentSection === null && (
|
||||
@@ -1288,76 +1428,66 @@ const SettingsScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* === ARIA Reparatur === */}
|
||||
{/* === Reparatur === */}
|
||||
<Text style={[styles.sectionTitle, {marginTop: 16}]}>Reparatur</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.toggleHint}>
|
||||
Wenn ARIA gar nicht mehr antwortet oder auf jede Anfrage mit
|
||||
"Antwort ohne Text" zurueckkommt — meistens ein steckengebliebener
|
||||
Run im aria-core. Dieser Button fuehrt {'“'}openclaw doctor --fix{'”'}
|
||||
aus und macht ARIA wieder ansprechbar.
|
||||
Container gezielt neu starten — wenn die Voice-Bridge, das Gehirn
|
||||
oder die Vector-DB haengt. Restart dauert wenige Sekunden,
|
||||
laufende Anfragen gehen verloren.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,149,0,0.15)'}]}
|
||||
onPress={() => {
|
||||
rvs.send('doctor_fix' as any, {});
|
||||
ToastAndroid.show('Reparatur-Befehl gesendet — Antwort kommt gleich', ToastAndroid.SHORT);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#FF9500'}]}>{'🔧 ARIA reparieren'}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.toggleHint, {marginTop: 12}]}>
|
||||
Wenn auch Reparieren nicht hilft — Container hart neu starten.
|
||||
ARIA ist dann ~15 Sekunden weg und kommt mit frischem State zurueck.
|
||||
Laufende Anfragen gehen verloren.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,59,48,0.15)'}]}
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'ARIA hart neu starten?',
|
||||
'Container-Restart (~15s). Laufende Anfragen gehen verloren.',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Neu starten', style: 'destructive', onPress: () => {
|
||||
rvs.send('aria_restart' as any, {});
|
||||
ToastAndroid.show('Container-Restart angestossen…', ToastAndroid.LONG);
|
||||
}},
|
||||
],
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>{'🚨 ARIA hart neu starten'}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.toggleHint, {marginTop: 12}]}>
|
||||
Konversation komplett zuruecksetzen — alle bisherigen Nachrichten
|
||||
aus ARIA's Session loeschen + Container neu. Anders als der harte
|
||||
Restart wird hier auch ARIA's Erinnerung an die laufende
|
||||
Konversation gewipt. Geschieht automatisch alle 140 Nachrichten
|
||||
(Bridge-Setting COMPACT_AFTER_MESSAGES).
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,149,0,0.15)'}]}
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'Konversation komprimieren?',
|
||||
'Alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf.',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Komprimieren', style: 'destructive', onPress: () => {
|
||||
rvs.send('aria_session_reset' as any, {});
|
||||
ToastAndroid.show('Session wird zurueckgesetzt…', ToastAndroid.LONG);
|
||||
}},
|
||||
],
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#FF9500'}]}>{'🧹 Konversation komprimieren'}</Text>
|
||||
</TouchableOpacity>
|
||||
{[
|
||||
{ name: 'aria-bridge', label: '🚨 aria-bridge neu (Voice + RVS)' },
|
||||
{ name: 'aria-brain', label: '🚨 aria-brain neu (Agent + Memory)' },
|
||||
{ name: 'aria-qdrant', label: '🚨 aria-qdrant neu (Vector-DB)' },
|
||||
].map(c => (
|
||||
<TouchableOpacity
|
||||
key={c.name}
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,59,48,0.10)'}]}
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
`${c.name} neu starten?`,
|
||||
'Restart in wenigen Sekunden. Laufende Anfragen gehen verloren.',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Neu starten', style: 'destructive', onPress: () => {
|
||||
rvs.send('container_restart' as any, { name: c.name });
|
||||
ToastAndroid.show(`${c.name} wird neu gestartet…`, ToastAndroid.LONG);
|
||||
}},
|
||||
],
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>{c.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
</>)}
|
||||
|
||||
{/* === Datei-Manager === */}
|
||||
{currentSection === 'files' && (<>
|
||||
<Text style={styles.sectionTitle}>Dateien</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.toggleHint}>
|
||||
Alle Dateien aus <Text style={{fontFamily:'monospace'}}>/shared/uploads/</Text>
|
||||
— was ARIA generiert hat und was du hochgeladen hast.
|
||||
Beim Löschen wird die Bubble in App + Diagnostic als gelöscht markiert.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(0,150,255,0.15)'}]}
|
||||
onPress={() => {
|
||||
setFileManagerError('');
|
||||
setFileManagerLoading(true);
|
||||
setFileManagerOpen(true);
|
||||
rvs.send('file_list_request' as any, {});
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.clearButtonText, {color: '#0096FF'}]}>{'📂 Datei-Manager öffnen'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>)}
|
||||
|
||||
{/* === Logs === */}
|
||||
{currentSection === 'protocol' && (<>
|
||||
<Text style={styles.sectionTitle}>Protokoll</Text>
|
||||
|
||||
Reference in New Issue
Block a user