Save and persist desk settings independently per workspace

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