feat(app): Kompakt ↔ Cockpit — Ansichts-Umschalter im Header (0.2.1.9)
Nach dem 0.2.1.8-Deploy sah die App "unveraendert" aus — korrekt, weil der Hauptchat nur eine Kachel hat (= Vollbild-Chat). Jetzt explizit umschaltbar: - services/viewMode.ts: 'compact' | 'cockpit', persistiert (aria_view_mode), Default 'compact' (nichts aendert sich fuer normale Nutzung). - ViewModeToggle im Navigations-Header (rechts, kollisionsfrei): "⧉ Kompakt" / "⧉ Cockpit". - WorkspaceScreen: compact → klassische ChatScreen direkt; cockpit → Canvas. - Canvas: Uebersicht/Gesten/Back jetzt auch bei einer Kachel erreichbar (der single-Force-Fokus entfaellt), damit sich Cockpit auch im Hauptchat wie ein Desktop anfuehlt. "⤢ Uebersicht"-Button nach unten rechts verschoben (weg von ChatScreens Kopf-Icons). Changelog: Workspace-Release als 0.2.1.8 gefuehrt, Umschalter als 0.2.1.9. tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -10,7 +10,17 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
||||
|
||||
---
|
||||
|
||||
## [0.2.2.0] — 2026-07-17 — Desktop-Workspace: zoombarer Canvas, Live-Code-Editor, QEMU/VNC
|
||||
## [0.2.1.9] — 2026-07-17 — Kompakt ↔ Cockpit: Umschalter für den Kachel-Desktop
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Ansichts-Umschalter im Header** („⧉ Kompakt" / „⧉ Cockpit"): Die App startet in **Kompakt** — der klassische Vollbild-Chat, **exakt wie vor dem Umbau** (Default, Mama-tauglich). Ein Tap auf den Button oben rechts schaltet auf **Cockpit** — den zoom-/verschiebbaren Kachel-Desktop. Persistiert über Neustart (`aria_view_mode`).
|
||||
- **Warum:** Nach dem 0.2.1.8-Deploy sah die App „unverändert" aus — korrekt, denn im normalen Chat gibt es nur eine Kachel (= Vollbild-Chat). Der Umschalter macht den Cockpit-Modus jetzt **explizit sichtbar/steuerbar**, statt nur bei Code-Projekten aufzutauchen.
|
||||
- Im Cockpit ist die **Übersicht jetzt immer erreichbar** (auch im Hauptchat mit nur einer Kachel): „⤢ Übersicht"-Button (unten rechts, weg von den Chat-Kopf-Icons), 2-Finger-Pinch/Pan, Hardware-Back.
|
||||
|
||||
---
|
||||
|
||||
## [0.2.1.8] — 2026-07-17 — Desktop-Workspace: zoombarer Canvas, Live-Code-Editor, QEMU/VNC
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
|
||||
import WorkspaceScreen from './src/workspace/WorkspaceScreen';
|
||||
import SettingsScreen from './src/screens/SettingsScreen';
|
||||
import ViewModeToggle from './src/components/ViewModeToggle';
|
||||
import rvs from './src/services/rvs';
|
||||
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
||||
import { acquireBackgroundAudio } from './src/services/backgroundAudio';
|
||||
@@ -170,6 +171,7 @@ const App: React.FC = () => {
|
||||
options={{
|
||||
title: 'ARIA Chat',
|
||||
headerTitle: 'ARIA Cockpit',
|
||||
headerRight: () => <ViewModeToggle />,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.2.1.8",
|
||||
"version": "0.2.1.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* ViewModeToggle — kleiner Header-Button zum Umschalten zwischen Kompakt-
|
||||
* Ansicht (klassischer Chat) und Cockpit (Kachel-Desktop).
|
||||
*
|
||||
* Sitzt rechts im Navigations-Header ("ARIA Cockpit"), kollidiert also mit
|
||||
* nichts in der Chat-Ansicht. Zeigt das Ziel des naechsten Taps.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
|
||||
import viewMode, { ViewModeValue } from '../services/viewMode';
|
||||
|
||||
const ViewModeToggle: React.FC = () => {
|
||||
const [mode, setMode] = useState<ViewModeValue>(viewMode.get());
|
||||
useEffect(() => viewMode.subscribe(setMode), []);
|
||||
|
||||
const isCockpit = mode === 'cockpit';
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => viewMode.toggle()}
|
||||
style={[styles.pill, isCockpit && styles.pillActive]}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
<Text style={[styles.text, isCockpit && styles.textActive]}>
|
||||
{isCockpit ? '⧉ Cockpit' : '⧉ Kompakt'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pill: {
|
||||
marginRight: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#1E1E2E',
|
||||
backgroundColor: '#12122A',
|
||||
},
|
||||
pillActive: { borderColor: '#0096FF', backgroundColor: '#0A1F33' },
|
||||
text: { color: '#9090B0', fontSize: 13, fontWeight: '700' },
|
||||
textActive: { color: '#0096FF' },
|
||||
});
|
||||
|
||||
export default ViewModeToggle;
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* viewMode — App-Ansicht: 'compact' (klassischer Vollbild-Chat wie vor 0.2.2.0)
|
||||
* oder 'cockpit' (zoombarer Kachel-Desktop).
|
||||
*
|
||||
* Default 'compact' → fuer normale Nutzung aendert sich nichts (Mama-tauglich).
|
||||
* Umschaltbar ueber den Header-Button; persistiert in AsyncStorage. Muster wie
|
||||
* services/rvs.ts (Singleton mit Listener-Liste).
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export type ViewModeValue = 'compact' | 'cockpit';
|
||||
|
||||
const KEY = 'aria_view_mode';
|
||||
type Sub = (mode: ViewModeValue) => void;
|
||||
|
||||
class ViewMode {
|
||||
private mode: ViewModeValue = 'compact';
|
||||
private subs: Sub[] = [];
|
||||
private loaded = false;
|
||||
|
||||
constructor() {
|
||||
AsyncStorage.getItem(KEY).then((v) => {
|
||||
if (v === 'cockpit' || v === 'compact') this.mode = v;
|
||||
this.loaded = true;
|
||||
this.emit();
|
||||
}).catch(() => { this.loaded = true; });
|
||||
}
|
||||
|
||||
get(): ViewModeValue { return this.mode; }
|
||||
isLoaded(): boolean { return this.loaded; }
|
||||
|
||||
set(mode: ViewModeValue): void {
|
||||
if (this.mode === mode) return;
|
||||
this.mode = mode;
|
||||
AsyncStorage.setItem(KEY, mode).catch(() => {});
|
||||
this.emit();
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.set(this.mode === 'compact' ? 'cockpit' : 'compact');
|
||||
}
|
||||
|
||||
subscribe(cb: Sub): () => void {
|
||||
this.subs.push(cb);
|
||||
cb(this.mode);
|
||||
return () => { this.subs = this.subs.filter((s) => s !== cb); };
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
const m = this.mode;
|
||||
this.subs.forEach((cb) => cb(m));
|
||||
}
|
||||
}
|
||||
|
||||
const viewMode = new ViewMode();
|
||||
export default viewMode;
|
||||
@@ -49,7 +49,6 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
const bounds = useMemo(() => boundsOf(visibleTiles), [visibleKey]);
|
||||
const worldW = bounds.w;
|
||||
const worldH = bounds.h;
|
||||
const single = visibleTiles.length <= 1;
|
||||
const { loaded: layoutLoaded, getFocus, saveFocus } = useWorkspaceLayout(projectId);
|
||||
|
||||
// Kamera (shared values fuer 60fps auf dem UI-Thread).
|
||||
@@ -60,21 +59,17 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
const savedTx = useSharedValue(0);
|
||||
const savedTy = useSharedValue(0);
|
||||
|
||||
// Bei nur einer Kachel: immer fokussiert. Verschwindet die fokussierte
|
||||
// Kachel aus der Sichtbarkeit, auf Chat zurueckfallen.
|
||||
// Verschwindet die fokussierte Kachel aus der Sichtbarkeit → auf Chat zurueck.
|
||||
useEffect(() => {
|
||||
if (single) {
|
||||
setFocusedTileId(visibleTiles[0] || 'chat');
|
||||
} else if (focusedTileId && !visibleTiles.includes(focusedTileId)) {
|
||||
if (focusedTileId && !visibleTiles.includes(focusedTileId)) {
|
||||
setFocusedTileId('chat');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visibleKey]);
|
||||
|
||||
// Zuletzt fokussierte Kachel pro Projekt wiederherstellen (nur Mehr-Kachel-
|
||||
// Projekte; laeuft NACH dem single-Effekt oben, gewinnt also fuer Code-Projekte).
|
||||
// Zuletzt fokussierte Kachel pro Projekt wiederherstellen.
|
||||
useEffect(() => {
|
||||
if (!layoutLoaded || single) return;
|
||||
if (!layoutLoaded) return;
|
||||
const stored = getFocus();
|
||||
if (stored === undefined) return;
|
||||
if (stored === null || visibleTiles.includes(stored)) setFocusedTileId(stored);
|
||||
@@ -101,10 +96,10 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [focusedTileId, visibleKey, vw, vh]);
|
||||
|
||||
// Hardware-Back: im Fokus (und mehr als eine Kachel) → zurueck zur Uebersicht.
|
||||
// Hardware-Back: im Fokus → zurueck zur Uebersicht.
|
||||
useEffect(() => {
|
||||
const onBack = () => {
|
||||
if (focusedTileId && !single) {
|
||||
if (focusedTileId) {
|
||||
setFocusedTileId(null);
|
||||
return true;
|
||||
}
|
||||
@@ -112,10 +107,10 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
};
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', onBack);
|
||||
return () => sub.remove();
|
||||
}, [focusedTileId, single]);
|
||||
}, [focusedTileId]);
|
||||
|
||||
// Gesten nur in der Uebersicht (Fokus-Modus: Touches fallen an die Kachel).
|
||||
const gesturesEnabled = !focusedTileId && !single;
|
||||
const gesturesEnabled = !focusedTileId;
|
||||
const canvasGesture = useMemo(() => {
|
||||
const pinch = Gesture.Pinch()
|
||||
.enabled(gesturesEnabled)
|
||||
@@ -194,7 +189,7 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
</View>
|
||||
|
||||
{/* Steuerung */}
|
||||
{focusedTileId && !single && (
|
||||
{focusedTileId && (
|
||||
<TouchableOpacity style={styles.overviewBtn} onPress={() => setFocusedTileId(null)} activeOpacity={0.8}>
|
||||
<Text style={styles.overviewBtnText}>⤢ Übersicht</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -211,16 +206,18 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
||||
const styles = StyleSheet.create({
|
||||
root: { flex: 1, backgroundColor: '#0D0D1A' },
|
||||
world: { position: 'absolute', left: 0, top: 0 },
|
||||
// Unten rechts (ueber der Chat-Eingabezeile) — weg von ChatScreens Kopf-Icons.
|
||||
overviewBtn: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 12,
|
||||
bottom: 88,
|
||||
right: 14,
|
||||
backgroundColor: 'rgba(18,18,42,0.92)',
|
||||
borderColor: '#1E1E2E',
|
||||
borderColor: '#0096FF',
|
||||
borderWidth: 1,
|
||||
borderRadius: 18,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
overviewBtnText: { color: '#0096FF', fontSize: 14, fontWeight: '700' },
|
||||
hintBar: {
|
||||
|
||||
@@ -14,14 +14,18 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
||||
import codeFile from '../services/codeFile';
|
||||
import desktop from '../services/desktop';
|
||||
import viewMode, { ViewModeValue } from '../services/viewMode';
|
||||
import ChatScreen from '../screens/ChatScreen';
|
||||
import { TileId, visibleTilesFor } from './layout';
|
||||
import WorkspaceCanvas from './WorkspaceCanvas';
|
||||
|
||||
const WorkspaceScreen: React.FC = () => {
|
||||
const [mode, setMode] = useState<ViewModeValue>(viewMode.get());
|
||||
const [focus, setFocus] = useState<FocusSnapshot>(projectFocus.get());
|
||||
const [hasCode, setHasCode] = useState(false);
|
||||
const [hasDesktop, setHasDesktop] = useState(false);
|
||||
|
||||
useEffect(() => viewMode.subscribe(setMode), []);
|
||||
useEffect(() => projectFocus.subscribe(setFocus), []);
|
||||
|
||||
const pid = focus.focusedProjectId;
|
||||
@@ -53,6 +57,12 @@ const WorkspaceScreen: React.FC = () => {
|
||||
[pid, focus.projectNameById],
|
||||
);
|
||||
|
||||
// Kompakt-Ansicht: klassischer Vollbild-Chat, exakt wie vor dem Umbau.
|
||||
if (mode === 'compact') {
|
||||
return <ChatScreen />;
|
||||
}
|
||||
|
||||
// Cockpit: zoombarer Kachel-Desktop.
|
||||
return <WorkspaceCanvas projectId={pid} visibleTiles={visibleTiles} subtitles={subtitles} />;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user