From ea3de70d05cb736fb3a90cbfe3fb4972fbe9acb4 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 17 Jul 2026 00:04:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(app):=20Workspace-Umbau=20Schritt=208=20?= =?UTF-8?q?=E2=80=94=20Live-Reveal=20+=20Layout-Persistenz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Progressive Reveal: ChatScreen spiegelt bei project_changed den Projekt-Typ sofort in projectFocus (kind_changed nach set_project_kind) → Editor/Desktop- Kacheln erscheinen live, nicht erst beim Reconnect. - useWorkspaceLayout: merkt pro Projekt die zuletzt fokussierte Kachel (AsyncStorage aria_workspace_layout) und stellt sie beim Zurueckkehren in ein Code-Projekt wieder her. tsc clean. Co-Authored-By: Claude Opus 4.8 --- android/src/screens/ChatScreen.tsx | 5 +++ android/src/workspace/WorkspaceCanvas.tsx | 18 ++++++++++ android/src/workspace/useWorkspaceLayout.ts | 40 +++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 android/src/workspace/useWorkspaceLayout.ts diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index bc44dc2..432fb79 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -987,6 +987,11 @@ const ChatScreen: React.FC = () => { if (p.id && p.name) { setProjectNameById(prev => ({ ...prev, [p.id]: p.name })); } + // Projekt-Typ sofort spiegeln → Editor/Desktop-Kacheln erscheinen live + // (z.B. nach set_project_kind), nicht erst beim naechsten Reconnect. + if (p.id && (p.kind === 'code' || p.kind === 'chat')) { + projectFocus.setKind(p.id, p.kind); + } if (action === 'entered' || action === 'created') { if (p.id) setFocusedProjectId(p.id); } else if (action === 'exited') { diff --git a/android/src/workspace/WorkspaceCanvas.tsx b/android/src/workspace/WorkspaceCanvas.tsx index 4ba8460..47b72d0 100644 --- a/android/src/workspace/WorkspaceCanvas.tsx +++ b/android/src/workspace/WorkspaceCanvas.tsx @@ -25,6 +25,7 @@ import { boundsOf, Camera, focusCamera, overviewCamera, TileId, TILE_RECTS, toLocal, } from './layout'; import Tile from './Tile'; +import { useWorkspaceLayout } from './useWorkspaceLayout'; import ChatTile from './tiles/ChatTile'; import CodeEditorTile from './tiles/CodeEditorTile'; import VncTile from './tiles/VncTile'; @@ -49,6 +50,7 @@ const WorkspaceCanvas: React.FC = ({ projectId, visibleTiles, subtitles } 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). const scale = useSharedValue(1); @@ -69,6 +71,22 @@ const WorkspaceCanvas: React.FC = ({ projectId, visibleTiles, subtitles } // 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). + useEffect(() => { + if (!layoutLoaded || single) return; + const stored = getFocus(); + if (stored === undefined) return; + if (stored === null || visibleTiles.includes(stored)) setFocusedTileId(stored); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectId, layoutLoaded, visibleKey]); + + // Fokus-Wahl persistieren. + useEffect(() => { + if (layoutLoaded) saveFocus(focusedTileId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [focusedTileId, layoutLoaded]); + // Kamera auf das Ziel fahren (Fokus-Rect oder Uebersicht). useEffect(() => { const cam: Camera = focusedTileId diff --git a/android/src/workspace/useWorkspaceLayout.ts b/android/src/workspace/useWorkspaceLayout.ts new file mode 100644 index 0000000..9d5006a --- /dev/null +++ b/android/src/workspace/useWorkspaceLayout.ts @@ -0,0 +1,40 @@ +/** + * useWorkspaceLayout — merkt sich pro Projekt die zuletzt fokussierte Kachel, + * damit man beim Zurueckkehren in ein Code-Projekt wieder dort landet (Editor/ + * Desktop) statt immer im Chat. Persistiert nach AsyncStorage (Muster wie + * aria_project_drafts). + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { TileId } from './layout'; + +const KEY = 'aria_workspace_layout'; + +interface Entry { focus: TileId | null } +type LayoutMap = Record; + +const keyOf = (projectId: string) => projectId || '__main__'; + +export function useWorkspaceLayout(projectId: string) { + const mapRef = useRef({}); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + AsyncStorage.getItem(KEY).then((v) => { + if (v) { try { mapRef.current = JSON.parse(v) || {}; } catch { /* ignore */ } } + setLoaded(true); + }).catch(() => setLoaded(true)); + }, []); + + const getFocus = useCallback((): TileId | null | undefined => { + return mapRef.current[keyOf(projectId)]?.focus; + }, [projectId]); + + const saveFocus = useCallback((focus: TileId | null) => { + mapRef.current = { ...mapRef.current, [keyOf(projectId)]: { focus } }; + AsyncStorage.setItem(KEY, JSON.stringify(mapRef.current)).catch(() => {}); + }, [projectId]); + + return { loaded, getFocus, saveFocus }; +}