feat(app): Projekte verstecken auch in der App (Auge + Toggle)

Spiegelt das Diagnostic-Feature auf App-Seite:
- Project-Interface + updateProject um hidden erweitert; setProjectHidden().
- ProjectsBrowser: versteckte Projekte standardmaessig ausgeblendet (mama
  sieht sie nicht). Auge pro Zeile (🙈 verstecken / 👁 sichtbar), Toggle
  "👁 Versteckte anzeigen (N)" blendet sie temporaer gedimmt + Badge ein
  zum Ansehen/Auswaehlen/Wieder-Sichtbarmachen. Eigener Touch am Auge, damit
  der Tap nicht das Projekt wechselt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:45:15 +02:00
co-authored by Claude Opus 4.8
parent 8a482f053e
commit 70b927eae7
2 changed files with 72 additions and 8 deletions
+61 -6
View File
@@ -75,6 +75,9 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
const [editing, setEditing] = useState<Project | null>(null); const [editing, setEditing] = useState<Project | null>(null);
const [editName, setEditName] = useState(''); const [editName, setEditName] = useState('');
const [editDesc, setEditDesc] = useState(''); const [editDesc, setEditDesc] = useState('');
// Versteckte Projekte standardmaessig ausblenden; Toggle blendet sie
// temporaer (gedimmt) ein — zum Ansehen/Auswaehlen oder Wieder-Sichtbarmachen.
const [showHidden, setShowHidden] = useState(false);
// Refs damit useCallback NICHT bei jeder Re-Render des Parents neu erzeugt // Refs damit useCallback NICHT bei jeder Re-Render des Parents neu erzeugt
// wird (parent uebergibt oft inline-arrow-Callbacks, neue Identity jedes // wird (parent uebergibt oft inline-arrow-Callbacks, neue Identity jedes
@@ -158,6 +161,12 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
]); ]);
}, [load]); }, [load]);
const toggleHidden = useCallback((p: Project) => {
brainApi.setProjectHidden(p.id, !p.hidden)
.then(() => load())
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
}, [load]);
const archiveProject = useCallback((p: Project) => { const archiveProject = useCallback((p: Project) => {
Alert.alert(`"${p.name}" archivieren?`, Alert.alert(`"${p.name}" archivieren?`,
'Verschwindet aus der Standardliste. Über "archivierte zeigen" erreichbar.', 'Verschwindet aus der Standardliste. Über "archivierte zeigen" erreichbar.',
@@ -176,11 +185,12 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
const renderItem = ({ item }: { item: Project }) => { const renderItem = ({ item }: { item: Project }) => {
const isActive = item.id === activeId; const isActive = item.id === activeId;
const dot = _statusDot(item.id); const dot = _statusDot(item.id);
const hidden = !!item.hidden;
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={() => switchTo(item.id)} onPress={() => switchTo(item.id)}
onLongPress={() => openEdit(item)} onLongPress={() => openEdit(item)}
style={[s.row, isActive && s.rowActive]} style={[s.row, isActive && s.rowActive, hidden && s.rowHidden]}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
@@ -188,6 +198,7 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} /> <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} />
)} )}
<Text style={[s.rowName, isActive && { color: '#34C759' }]}>{item.name}</Text> <Text style={[s.rowName, isActive && { color: '#34C759' }]}>{item.name}</Text>
{hidden && <Text style={s.hiddenBadge}>versteckt</Text>}
{item.status === 'ended' && <Text style={s.statusBadge}>beendet</Text>} {item.status === 'ended' && <Text style={s.statusBadge}>beendet</Text>}
{isActive && <Text style={s.activeBadge}>✓ FOCUS</Text>} {isActive && <Text style={s.activeBadge}>✓ FOCUS</Text>}
</View> </View>
@@ -199,10 +210,22 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
{dot.label ? ` · ${dot.label}` : ''} {dot.label ? ` · ${dot.label}` : ''}
</Text> </Text>
</View> </View>
{/* Auge: verstecken (🙈) / wieder sichtbar (👁). Eigener Touch, damit
der Tap NICHT das Projekt wechselt. */}
<TouchableOpacity
onPress={() => toggleHidden(item)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={s.eyeBtn}
>
<Text style={s.eyeIcon}>{hidden ? '👁' : '🙈'}</Text>
</TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
const hiddenCount = projects.filter(p => p.hidden).length;
const visibleProjects = showHidden ? projects : projects.filter(p => !p.hidden);
const body = ( const body = (
<View style={{ flex: 1, backgroundColor: '#0A0A14' }}> <View style={{ flex: 1, backgroundColor: '#0A0A14' }}>
{/* Header */} {/* Header */}
@@ -243,6 +266,17 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
); );
})()} })()}
{/* Versteckte-Toggle — nur wenn es welche gibt (oder gerade eingeblendet) */}
{(hiddenCount > 0 || showHidden) && (
<TouchableOpacity onPress={() => setShowHidden(v => !v)} style={s.hiddenToggle}>
<Text style={s.hiddenToggleText}>
{showHidden
? `🙈 Versteckte ausblenden${hiddenCount ? ` (${hiddenCount})` : ''}`
: `👁 Versteckte anzeigen${hiddenCount ? ` (${hiddenCount})` : ''}`}
</Text>
</TouchableOpacity>
)}
{loading ? ( {loading ? (
<View style={{ padding: 24, alignItems: 'center' }}> <View style={{ padding: 24, alignItems: 'center' }}>
<ActivityIndicator color="#0096FF" /> <ActivityIndicator color="#0096FF" />
@@ -251,14 +285,21 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
<Text style={s.errorText}>⚠ {err}</Text> <Text style={s.errorText}>⚠ {err}</Text>
) : ( ) : (
<FlatList <FlatList
data={projects} data={visibleProjects}
keyExtractor={p => p.id} keyExtractor={p => p.id}
renderItem={renderItem} renderItem={renderItem}
ListEmptyComponent={ ListEmptyComponent={
<Text style={s.emptyText}> projects.length > 0 ? (
Noch keine Projekte. Tipp + Neu oder sag zu ARIA:{'\n'} <Text style={s.emptyText}>
„Lass uns ein Projekt 'XY' anlegen". Alle {hiddenCount} Projekte sind versteckt.{'\n'}
</Text> Tipp „👁 Versteckte anzeigen".
</Text>
) : (
<Text style={s.emptyText}>
Noch keine Projekte. Tipp + Neu oder sag zu ARIA:{'\n'}
„Lass uns ein Projekt 'XY' anlegen".
</Text>
)
} }
/> />
)} )}
@@ -365,6 +406,8 @@ const s = StyleSheet.create({
headerBtnText: { color: '#0096FF', fontSize: 18, fontWeight: '600' }, headerBtnText: { color: '#0096FF', fontSize: 18, fontWeight: '600' },
headerTitle: { flex: 1, textAlign: 'center', color: '#E0E0F0', fontSize: 18, fontWeight: '700' }, headerTitle: { flex: 1, textAlign: 'center', color: '#E0E0F0', fontSize: 18, fontWeight: '700' },
row: { row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
@@ -375,6 +418,18 @@ const s = StyleSheet.create({
borderLeftWidth: 3, borderLeftWidth: 3,
borderLeftColor: '#34C759', borderLeftColor: '#34C759',
}, },
rowHidden: { opacity: 0.55 },
eyeBtn: { paddingHorizontal: 8, paddingVertical: 6, marginLeft: 6 },
eyeIcon: { fontSize: 18 },
hiddenBadge: { color: '#B392F0', fontSize: 10, fontWeight: '700',
backgroundColor: 'rgba(179,146,240,0.15)', paddingHorizontal: 6,
paddingVertical: 2, borderRadius: 4 },
hiddenToggle: {
paddingHorizontal: 16, paddingVertical: 10,
borderBottomWidth: 1, borderColor: '#1E1E2E',
backgroundColor: '#0D0D18',
},
hiddenToggleText: { color: '#B392F0', fontSize: 12, fontWeight: '600' },
rowName: { color: '#E0E0F0', fontSize: 16, fontWeight: '600' }, rowName: { color: '#E0E0F0', fontSize: 16, fontWeight: '600' },
rowDesc: { color: '#8888AA', fontSize: 13, marginTop: 4 }, rowDesc: { color: '#8888AA', fontSize: 13, marginTop: 4 },
rowMeta: { color: '#555570', fontSize: 11, marginTop: 4 }, rowMeta: { color: '#555570', fontSize: 11, marginTop: 4 },
+11 -2
View File
@@ -157,6 +157,7 @@ export interface Project {
name: string; name: string;
description: string; description: string;
status: 'active' | 'ended' | 'archived'; status: 'active' | 'ended' | 'archived';
hidden?: boolean; // aus Listen ausgeblendet (bleibt nutzbar)
created_at: number; created_at: number;
updated_at: number; updated_at: number;
last_activity_at: number; last_activity_at: number;
@@ -593,14 +594,22 @@ export const brainApi = {
}); });
}, },
/** Projekt-Metadaten patchen (name / description). */ /** Projekt-Metadaten patchen (name / description / hidden). */
updateProject(projectId: string, patch: Partial<Pick<Project, 'name' | 'description'>>): Promise<Project> { updateProject(projectId: string, patch: Partial<Pick<Project, 'name' | 'description' | 'hidden'>>): Promise<Project> {
return _send(`/projects/${encodeURIComponent(projectId)}`, { return _send(`/projects/${encodeURIComponent(projectId)}`, {
method: 'PATCH', method: 'PATCH',
body: patch, body: patch,
}); });
}, },
/** Projekt verstecken / wieder sichtbar machen (bleibt voll nutzbar). */
setProjectHidden(projectId: string, hidden: boolean): Promise<Project> {
return _send(`/projects/${encodeURIComponent(projectId)}`, {
method: 'PATCH',
body: { hidden },
});
},
/** Queue-Status: pro Kontext (project_id oder __main__ fuer Hauptchat) /** Queue-Status: pro Kontext (project_id oder __main__ fuer Hauptchat)
* ob gerade ein Request in Verarbeitung ist + wieviele in der Queue warten. * ob gerade ein Request in Verarbeitung ist + wieviele in der Queue warten.
* Wird fuer Status-Dots im Drawer periodisch gepollt. */ * Wird fuer Status-Dots im Drawer periodisch gepollt. */