From 760ba2ff53a3a001ac21cfff5c43ce092ffd1874 Mon Sep 17 00:00:00 2001 From: airent106 Date: Tue, 13 Oct 2026 11:53:06 -0600 Subject: [PATCH] Save and persist desk settings independently per workspace --- src/App.tsx | 315 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 191 insertions(+), 124 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3939ffb..7503ffa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,7 +76,7 @@ export const App: React.FC = () => { } const currentBoard = targetBoard || currentSettings.deskTexture || 'oak'; - const updatedBoards = targetBoards || { + const updatedBoards: BoardsMap = targetBoards || { ...boardsRef.current, [currentBoard]: { cards: currentCards, @@ -85,6 +85,14 @@ export const App: React.FC = () => { settings: currentSettings } }; + if (!targetBoards) { + updatedBoards[currentBoard] = { + cards: currentCards, + props: currentProps, + strokes: currentStrokes, + settings: currentSettings + }; + } boardsRef.current = updatedBoards; const doSave = () => { @@ -98,13 +106,7 @@ export const App: React.FC = () => { strokes: currentStrokes, activeBoard: currentBoard, boards: updatedBoards, - settings: { - deskTexture: currentBoard, - lightAngle: currentSettings.lightAngle, - grain: currentSettings.grain, - layoutMode: currentSettings.layoutMode, - showEvidenceStrings: currentSettings.showEvidenceStrings - }, + settings: currentSettings, originClientId: clientIdRef.current }) }) @@ -161,13 +163,23 @@ export const App: React.FC = () => { if (currentBoardData.props) setPropsList(currentBoardData.props); if (currentBoardData.strokes) setStrokes(currentBoardData.strokes); + const boardSettings = currentBoardData.settings || desk.settings || {}; + const isMobile = window.innerWidth < 768; setSettings((s) => ({ ...s, + ...boardSettings, deskTexture: activeBoard, - lightAngle: desk.settings?.lightAngle ?? s.lightAngle, - grain: desk.settings?.grain ?? s.grain, - layoutMode: desk.settings?.layoutMode ?? s.layoutMode, - showEvidenceStrings: activeBoard === 'evidence-board' ? true : (desk.settings?.showEvidenceStrings ?? false) + zoom: boardSettings.zoom ?? (isMobile ? 0.65 : 1.0), + layoutMode: boardSettings.layoutMode ?? 'scatter', + isLocked: boardSettings.isLocked ?? false, + fontMode: boardSettings.fontMode ?? (activeBoard === 'blueprint' ? 'typed' : 'mixed'), + lightAngle: boardSettings.lightAngle ?? (activeBoard === 'blueprint' ? 270 : 225), + grain: boardSettings.grain ?? s.grain, + showEvidenceStrings: boardSettings.showEvidenceStrings ?? (activeBoard === 'evidence-board'), + showProps: boardSettings.showProps ?? true, + cardCount: boardSettings.cardCount ?? (currentBoardData.cards ? currentBoardData.cards.length : 22), + activeFilter: boardSettings.activeFilter ?? 'all', + soundEnabled: desk.settings?.soundEnabled ?? s.soundEnabled })); const maxZ = Math.max(15, ...(currentBoardData.cards || []).map((c: CardItem) => c.zIndex || 1)); @@ -190,7 +202,7 @@ export const App: React.FC = () => { ); setCards(initialCards); const initialBoards: BoardsMap = { - oak: { cards: initialCards, props: INITIAL_PROPS, strokes: [] } + oak: { cards: initialCards, props: INITIAL_PROPS, strokes: [], settings } }; boardsRef.current = initialBoards; setMaxZIndex(initialCards.length + 20); @@ -232,14 +244,23 @@ export const App: React.FC = () => { } if (Array.isArray(payload.desk.props)) setPropsList(payload.desk.props); if (Array.isArray(payload.desk.strokes)) setStrokes(payload.desk.strokes); - if (payload.desk.settings) { + const boardSettings = (payload.desk.boards && payload.desk.boards[activeBoard]?.settings) || payload.desk.settings; + if (boardSettings) { setSettings((prev) => ({ ...prev, + ...boardSettings, deskTexture: activeBoard, - lightAngle: payload.desk.settings.lightAngle ?? prev.lightAngle, - grain: payload.desk.settings.grain ?? prev.grain, - layoutMode: payload.desk.settings.layoutMode ?? prev.layoutMode, - showEvidenceStrings: payload.desk.settings.showEvidenceStrings ?? prev.showEvidenceStrings + zoom: boardSettings.zoom ?? prev.zoom, + layoutMode: boardSettings.layoutMode ?? prev.layoutMode, + isLocked: boardSettings.isLocked ?? prev.isLocked, + fontMode: boardSettings.fontMode ?? prev.fontMode, + lightAngle: boardSettings.lightAngle ?? prev.lightAngle, + grain: boardSettings.grain ?? prev.grain, + showEvidenceStrings: boardSettings.showEvidenceStrings ?? prev.showEvidenceStrings, + showProps: boardSettings.showProps ?? prev.showProps, + cardCount: boardSettings.cardCount ?? prev.cardCount, + activeFilter: boardSettings.activeFilter ?? prev.activeFilter, + soundEnabled: prev.soundEnabled })); } setSyncStatus('synced'); @@ -262,68 +283,101 @@ export const App: React.FC = () => { }, []); // Update desk settings or switch unique workspace board - const updateSettings = (newSettings: Partial) => { - setSettings((prev) => { - const updated = { ...prev, ...newSettings }; + const updateSettings = useCallback( + (newSettingsInput: Partial | ((prev: DeskSettings) => Partial)) => { + setSettings((prev) => { + const newSettings = typeof newSettingsInput === 'function' ? newSettingsInput(prev) : newSettingsInput; + const updated = { ...prev, ...newSettings }; - // 1. Switch between 8 unique workspace boards - if (newSettings.deskTexture !== undefined && newSettings.deskTexture !== prev.deskTexture) { - const prevDesk = prev.deskTexture; - const nextDesk = newSettings.deskTexture; + // 1. Switch between 8 unique workspace boards + if (newSettings.deskTexture !== undefined && newSettings.deskTexture !== prev.deskTexture) { + const prevDesk = prev.deskTexture; + const nextDesk = newSettings.deskTexture; - // Snapshot current board state - const updatedBoards = { - ...boardsRef.current, - [prevDesk]: { - cards, - props: propsList, - strokes, - settings: prev + // Snapshot current board state with its active settings + const updatedBoards: BoardsMap = { + ...boardsRef.current, + [prevDesk]: { + cards, + props: propsList, + strokes, + settings: prev + } + }; + + // Load or initialize target board state + let nextBoardData = updatedBoards[nextDesk]; + if (!nextBoardData || !Array.isArray(nextBoardData.cards) || nextBoardData.cards.length === 0) { + if (nextDesk === 'oak') { + const w = window.innerWidth; + const h = window.innerHeight; + const seed = generateProceduralCards(w < 768 ? 9 : 20, w, h, prev.attachChance, prev.fontMode); + nextBoardData = { + cards: seed, + props: INITIAL_PROPS, + strokes: [], + settings: { deskTexture: 'oak', zoom: w < 768 ? 0.65 : 1.0, lightAngle: 225, grain: 0.7 } + }; + } else { + nextBoardData = getDefaultBoardData(nextDesk, window.innerWidth, window.innerHeight); + } + updatedBoards[nextDesk] = nextBoardData; } - }; - // Load or initialize target board state - let nextBoardData = updatedBoards[nextDesk]; - if (!nextBoardData || !Array.isArray(nextBoardData.cards) || nextBoardData.cards.length === 0) { - if (nextDesk === 'oak') { - const w = window.innerWidth; - const h = window.innerHeight; - const seed = generateProceduralCards(w < 768 ? 9 : 20, w, h, updated.attachChance, updated.fontMode); - nextBoardData = { cards: seed, props: INITIAL_PROPS, strokes: [] }; - } else { - nextBoardData = getDefaultBoardData(nextDesk, window.innerWidth, window.innerHeight); - } - updatedBoards[nextDesk] = nextBoardData; + // Restore target workspace's own saved settings + const targetSavedSettings = nextBoardData.settings || {}; + const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; + + const nextSettings: DeskSettings = { + ...prev, + ...targetSavedSettings, + ...newSettings, + deskTexture: nextDesk, + zoom: newSettings.zoom ?? targetSavedSettings.zoom ?? (isMobile ? 0.65 : 1.0), + layoutMode: newSettings.layoutMode ?? targetSavedSettings.layoutMode ?? 'scatter', + isLocked: newSettings.isLocked ?? targetSavedSettings.isLocked ?? false, + fontMode: newSettings.fontMode ?? targetSavedSettings.fontMode ?? (nextDesk === 'blueprint' ? 'typed' : 'mixed'), + lightAngle: newSettings.lightAngle ?? targetSavedSettings.lightAngle ?? (nextDesk === 'blueprint' ? 270 : 225), + grain: newSettings.grain ?? targetSavedSettings.grain ?? prev.grain, + showEvidenceStrings: newSettings.showEvidenceStrings ?? targetSavedSettings.showEvidenceStrings ?? (nextDesk === 'evidence-board'), + showProps: newSettings.showProps ?? targetSavedSettings.showProps ?? true, + cardCount: newSettings.cardCount ?? targetSavedSettings.cardCount ?? (nextBoardData.cards ? nextBoardData.cards.length : prev.cardCount), + activeFilter: newSettings.activeFilter ?? targetSavedSettings.activeFilter ?? 'all', + // Preserve user global audio preference: + soundEnabled: prev.soundEnabled + }; + + updatedBoards[nextDesk] = { + cards: nextBoardData.cards || [], + props: nextBoardData.props || INITIAL_PROPS, + strokes: nextBoardData.strokes || [], + settings: nextSettings + }; + + boardsRef.current = updatedBoards; + setCards(nextBoardData.cards || []); + setPropsList(nextBoardData.props || INITIAL_PROPS); + setStrokes(nextBoardData.strokes || []); + + saveToServer( + nextBoardData.cards || [], + nextBoardData.props || INITIAL_PROPS, + nextBoardData.strokes || [], + nextSettings, + true, + nextDesk, + updatedBoards + ); + + soundEngine.rustle(nextSettings.soundEnabled); + return nextSettings; } - boardsRef.current = updatedBoards; - setCards(nextBoardData.cards || []); - setPropsList(nextBoardData.props || INITIAL_PROPS); - setStrokes(nextBoardData.strokes || []); - - const nextEvidenceStrings = nextDesk === 'evidence-board' ? true : (newSettings.showEvidenceStrings ?? false); - - updated.deskTexture = nextDesk; - updated.showEvidenceStrings = nextEvidenceStrings; - - saveToServer( - nextBoardData.cards || [], - nextBoardData.props || INITIAL_PROPS, - nextBoardData.strokes || [], - updated, - true, - nextDesk, - updatedBoards - ); - - soundEngine.rustle(updated.soundEnabled); - } - - // 2. Real-time typography mode update across desk cards - if (newSettings.fontMode !== undefined && newSettings.fontMode !== prev.fontMode) { - const mode = newSettings.fontMode; - setCards((prevCards) => { - const next = prevCards.map((c) => { + // 2. Real-time typography mode update across desk cards + let nextCards = cards; + if (newSettings.fontMode !== undefined && newSettings.fontMode !== prev.fontMode) { + const mode = newSettings.fontMode; + nextCards = cards.map((c) => { const fonts = mode === 'handwritten' ? ['font-caveat', 'font-reenie', 'font-indie', 'font-shadows'] : mode === 'typed' @@ -332,40 +386,55 @@ export const App: React.FC = () => { const chosen = fonts[Math.floor(Math.random() * fonts.length)]; return { ...c, fontFamily: chosen }; }); - saveToServer(next, propsList, strokes, updated, false); - return next; - }); - } - - // 3. Card density adjustment - if (newSettings.cardCount !== undefined && newSettings.cardCount !== prev.cardCount) { - const w = window.innerWidth; - const h = window.innerHeight; - const diff = newSettings.cardCount - cards.length; - if (diff > 0) { - const added = generateProceduralCards(diff, w, h, updated.attachChance, updated.fontMode); - setCards((c) => [...c, ...added]); - } else if (diff < 0) { - setCards((c) => c.slice(0, newSettings.cardCount)); + setCards(nextCards); } - } - // 4. Attach chance adjustment - if (newSettings.attachChance !== undefined && newSettings.attachChance !== prev.attachChance) { - setCards((c) => - c.map((card) => { + // 3. Card density adjustment + if (newSettings.cardCount !== undefined && newSettings.cardCount !== prev.cardCount) { + const w = window.innerWidth; + const h = window.innerHeight; + const diff = newSettings.cardCount - nextCards.length; + if (diff > 0) { + const added = generateProceduralCards(diff, w, h, updated.attachChance, updated.fontMode); + nextCards = [...nextCards, ...added]; + setCards(nextCards); + } else if (diff < 0) { + nextCards = nextCards.slice(0, newSettings.cardCount); + setCards(nextCards); + } + } + + // 4. Attach chance adjustment + if (newSettings.attachChance !== undefined && newSettings.attachChance !== prev.attachChance) { + nextCards = nextCards.map((card) => { if (Math.random() < (newSettings.attachChance ?? 0.55)) { const types: CardItem['attachType'][] = ['washi', 'pin', 'paperclip']; return { ...card, attachType: types[Math.floor(Math.random() * types.length)] }; } return { ...card, attachType: 'none' }; - }) - ); - } + }); + setCards(nextCards); + } - return updated; - }); - }; + // 5. Snapshot and persist current workspace's updated settings + const currentDesk = prev.deskTexture; + const updatedBoards: BoardsMap = { + ...boardsRef.current, + [currentDesk]: { + cards: nextCards, + props: propsList, + strokes, + settings: updated + } + }; + boardsRef.current = updatedBoards; + + saveToServer(nextCards, propsList, strokes, updated, false, currentDesk, updatedBoards); + return updated; + }); + }, + [cards, propsList, strokes, saveToServer] + ); // Re-shuffle cards (stratified positioning so cards never clump or run over each other) const handleShuffle = useCallback(() => { @@ -453,14 +522,17 @@ export const App: React.FC = () => { }); }, [settings.zoom, settings.soundEnabled, saveToServer, propsList, strokes, settings]); - // Trigger re-layout when layoutMode or zoom changes + // Trigger re-layout when layoutMode changes to grid + const prevLayoutModeRef = useRef(settings.layoutMode); useEffect(() => { - if (settings.layoutMode === 'grid') { - arrangeGrid(); - } else { - handleShuffle(); + if (!isLoadedRef.current) return; + if (prevLayoutModeRef.current !== settings.layoutMode) { + prevLayoutModeRef.current = settings.layoutMode; + if (settings.layoutMode === 'grid') { + arrangeGrid(); + } } - }, [settings.layoutMode, settings.zoom]); + }, [settings.layoutMode, arrangeGrid]); // Physics animation loop on requestAnimationFrame useEffect(() => { @@ -672,16 +744,15 @@ export const App: React.FC = () => { handleShuffle(); } else if (e.key === 'g' || e.key === 'G') { e.preventDefault(); - setSettings((prev) => ({ - ...prev, + updateSettings((prev) => ({ layoutMode: prev.layoutMode === 'scatter' ? 'grid' : 'scatter' })); } else if (e.key === 'p' || e.key === 'P') { e.preventDefault(); - setSettings((prev) => ({ ...prev, penMode: !prev.penMode })); + updateSettings((prev) => ({ penMode: !prev.penMode })); } else if (e.key === 'l' || e.key === 'L') { e.preventDefault(); - setSettings((prev) => ({ ...prev, isLocked: !prev.isLocked })); + updateSettings((prev) => ({ isLocked: !prev.isLocked })); } else if (e.key === 'n' || e.key === 'N') { e.preventDefault(); setIsAddOpen(true); @@ -693,7 +764,7 @@ export const App: React.FC = () => { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [handleShuffle]); + }, [handleShuffle, updateSettings]); // Load Presets const handleLoadPreset = (presetName: string) => { @@ -703,7 +774,7 @@ export const App: React.FC = () => { if (presetName === 'original') { const generated = generateProceduralCards(20, w, h, 0.6, 'mixed'); setCards(generated); - setSettings((s) => ({ ...s, deskTexture: 'oak', lightAngle: 225, grain: 0.7 })); + updateSettings({ deskTexture: 'oak', lightAngle: 225, grain: 0.7 }); } else if (presetName === 'designer') { const designerCards: CardItem[] = SAMPLE_PRESETS.slice(0, 14).map((p, i) => ({ id: `design-${i}`, @@ -725,7 +796,7 @@ export const App: React.FC = () => { todoItems: p.todoItems })); setCards(designerCards); - setSettings((s) => ({ ...s, deskTexture: 'cutting-mat', lightAngle: 180 })); + updateSettings({ deskTexture: 'cutting-mat', lightAngle: 180 }); } else if (presetName === 'creative-coder') { const coderCards: CardItem[] = [ { @@ -784,7 +855,7 @@ export const App: React.FC = () => { } ]; setCards(coderCards); - setSettings((s) => ({ ...s, deskTexture: 'blueprint', lightAngle: 270 })); + updateSettings({ deskTexture: 'blueprint', lightAngle: 270 }); } else if (presetName === 'photojournal') { const photoCards = SAMPLE_PRESETS.filter((p) => p.type === 'polaroid').map((p, i) => ({ id: `photo-${i}`, @@ -806,7 +877,7 @@ export const App: React.FC = () => { fontFamily: 'font-caveat' })); setCards(photoCards); - setSettings((s) => ({ ...s, deskTexture: 'walnut', lightAngle: 225 })); + updateSettings({ deskTexture: 'walnut', lightAngle: 225 }); } soundEngine.shuffle(settings.soundEnabled); }; @@ -963,20 +1034,19 @@ export const App: React.FC = () => { - setSettings((prev) => ({ - ...prev, + updateSettings((prev) => ({ layoutMode: prev.layoutMode === 'scatter' ? 'grid' : 'scatter' })) } - onTogglePen={() => setSettings((prev) => ({ ...prev, penMode: !prev.penMode }))} - onToggleLock={() => setSettings((prev) => ({ ...prev, isLocked: !prev.isLocked }))} + onTogglePen={() => updateSettings((prev) => ({ penMode: !prev.penMode }))} + onToggleLock={() => updateSettings((prev) => ({ isLocked: !prev.isLocked }))} onOpenSettings={() => setIsSettingsOpen(true)} onAddCard={() => setIsAddOpen(true)} onCycleZoom={() => - setSettings((prev) => { + updateSettings((prev) => { const z = prev.zoom; const nextZoom = z <= 0.55 ? 0.75 : z <= 0.8 ? 1.0 : 0.5; - return { ...prev, zoom: nextZoom }; + return { zoom: nextZoom }; }) } penActive={settings.penMode} @@ -1062,9 +1132,6 @@ export const App: React.FC = () => { settings={settings} onLoadPreset={(name) => { handleLoadPreset(name); - setTimeout(() => { - saveToServer(cards, propsList, strokes, settings, true); - }, 100); }} onImportJson={(importedCards) => { setCards(importedCards);