feat(app): Workspace-Umbau Schritt 8 — Live-Reveal + Layout-Persistenz
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -987,6 +987,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
if (p.id && p.name) {
|
if (p.id && p.name) {
|
||||||
setProjectNameById(prev => ({ ...prev, [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 (action === 'entered' || action === 'created') {
|
||||||
if (p.id) setFocusedProjectId(p.id);
|
if (p.id) setFocusedProjectId(p.id);
|
||||||
} else if (action === 'exited') {
|
} else if (action === 'exited') {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
boundsOf, Camera, focusCamera, overviewCamera, TileId, TILE_RECTS, toLocal,
|
boundsOf, Camera, focusCamera, overviewCamera, TileId, TILE_RECTS, toLocal,
|
||||||
} from './layout';
|
} from './layout';
|
||||||
import Tile from './Tile';
|
import Tile from './Tile';
|
||||||
|
import { useWorkspaceLayout } from './useWorkspaceLayout';
|
||||||
import ChatTile from './tiles/ChatTile';
|
import ChatTile from './tiles/ChatTile';
|
||||||
import CodeEditorTile from './tiles/CodeEditorTile';
|
import CodeEditorTile from './tiles/CodeEditorTile';
|
||||||
import VncTile from './tiles/VncTile';
|
import VncTile from './tiles/VncTile';
|
||||||
@@ -49,6 +50,7 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
|||||||
const worldW = bounds.w;
|
const worldW = bounds.w;
|
||||||
const worldH = bounds.h;
|
const worldH = bounds.h;
|
||||||
const single = visibleTiles.length <= 1;
|
const single = visibleTiles.length <= 1;
|
||||||
|
const { loaded: layoutLoaded, getFocus, saveFocus } = useWorkspaceLayout(projectId);
|
||||||
|
|
||||||
// Kamera (shared values fuer 60fps auf dem UI-Thread).
|
// Kamera (shared values fuer 60fps auf dem UI-Thread).
|
||||||
const scale = useSharedValue(1);
|
const scale = useSharedValue(1);
|
||||||
@@ -69,6 +71,22 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [visibleKey]);
|
}, [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).
|
// Kamera auf das Ziel fahren (Fokus-Rect oder Uebersicht).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cam: Camera = focusedTileId
|
const cam: Camera = focusedTileId
|
||||||
|
|||||||
@@ -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<string, Entry>;
|
||||||
|
|
||||||
|
const keyOf = (projectId: string) => projectId || '__main__';
|
||||||
|
|
||||||
|
export function useWorkspaceLayout(projectId: string) {
|
||||||
|
const mapRef = useRef<LayoutMap>({});
|
||||||
|
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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user