diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a7bc2..1a5b017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/android/App.tsx b/android/App.tsx index 8b4a64b..2baf3f8 100644 --- a/android/App.tsx +++ b/android/App.tsx @@ -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: () => , }} /> { + const [mode, setMode] = useState(viewMode.get()); + useEffect(() => viewMode.subscribe(setMode), []); + + const isCockpit = mode === 'cockpit'; + return ( + viewMode.toggle()} + style={[styles.pill, isCockpit && styles.pillActive]} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + activeOpacity={0.75} + > + + {isCockpit ? '⧉ Cockpit' : '⧉ Kompakt'} + + + ); +}; + +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; diff --git a/android/src/services/viewMode.ts b/android/src/services/viewMode.ts new file mode 100644 index 0000000..cb5f46c --- /dev/null +++ b/android/src/services/viewMode.ts @@ -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; diff --git a/android/src/workspace/WorkspaceCanvas.tsx b/android/src/workspace/WorkspaceCanvas.tsx index 47b72d0..04394f0 100644 --- a/android/src/workspace/WorkspaceCanvas.tsx +++ b/android/src/workspace/WorkspaceCanvas.tsx @@ -49,7 +49,6 @@ const WorkspaceCanvas: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ projectId, visibleTiles, subtitles } {/* Steuerung */} - {focusedTileId && !single && ( + {focusedTileId && ( setFocusedTileId(null)} activeOpacity={0.8}> ⤢ Übersicht @@ -211,16 +206,18 @@ const WorkspaceCanvas: React.FC = ({ 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: { diff --git a/android/src/workspace/WorkspaceScreen.tsx b/android/src/workspace/WorkspaceScreen.tsx index e03c4b9..dae99dc 100644 --- a/android/src/workspace/WorkspaceScreen.tsx +++ b/android/src/workspace/WorkspaceScreen.tsx @@ -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(viewMode.get()); const [focus, setFocus] = useState(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 ; + } + + // Cockpit: zoombarer Kachel-Desktop. return ; };