diff --git a/src/App.tsx b/src/App.tsx index f2013a34..1c8db332 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -110,7 +110,8 @@ function App() { const clientInitialized = useRef(false); const hapticsRuntimeRef = useRef(null); - const preferences = usePreferences(); + const midiEnabled = usePreferences((state) => state.midi.enabled); + const hapticsEnabled = usePreferences((state) => state.haptics.enabled); const connected = useConnectionStore((state) => state.connected); const sessionReady = useConnectionStore((state) => state.sessionReady); useFileTransferNotifications(client); @@ -334,7 +335,7 @@ function App() { }, [handleAppKeyDown]); useEffect(() => { - if (!preferences.midi.enabled) return; + if (!midiEnabled) return; let cancelled = false; import("./VirtualMidiService") @@ -355,7 +356,7 @@ function App() { return () => { cancelled = true; }; - }, [preferences.midi.enabled]); + }, [midiEnabled]); // Window subtitle tracks the current room from the room store. On disconnect // the client resets the store, which clears roomInfo and so clears the subtitle. @@ -372,8 +373,8 @@ function App() { }, [client, roomInfo]); useEffect(() => { - hapticsRuntimeRef.current?.setEnabled(preferences.haptics.enabled); - }, [preferences.haptics.enabled]); + hapticsRuntimeRef.current?.setEnabled(hapticsEnabled); + }, [hapticsEnabled]); const handleCommand = useCallback( (text: string) => { diff --git a/src/audio/MediaService.ts b/src/audio/MediaService.ts index c062b492..dbef624e 100644 --- a/src/audio/MediaService.ts +++ b/src/audio/MediaService.ts @@ -175,9 +175,12 @@ export class MediaService { window.addEventListener('blur', this.handleWindowBlur); } - this.unsubscribePreferences = usePreferences.subscribe(() => { - this.updateBackgroundMuteState(); - }); + this.unsubscribePreferences = usePreferences.subscribe( + (state) => state.sound.muteInBackground, + () => { + this.updateBackgroundMuteState(); + }, + ); } get muted(): boolean { diff --git a/src/components/HapticsStatus.tsx b/src/components/HapticsStatus.tsx index a2445cf5..b86eece6 100644 --- a/src/components/HapticsStatus.tsx +++ b/src/components/HapticsStatus.tsx @@ -10,7 +10,7 @@ interface HapticsStatusProps { } const HapticsStatus: React.FC = () => { - const preferences = usePreferences(); + const hapticsPreferences = usePreferences((state) => state.haptics); const [capabilities, setCapabilities] = useState( hapticsService.getCapabilities() ); @@ -61,7 +61,7 @@ const HapticsStatus: React.FC = () => { } }; - if (!preferences.haptics.enabled) { + if (!hapticsPreferences.enabled) { return (

Haptics is disabled. Enable it in preferences to use haptics features.

@@ -252,7 +252,7 @@ const HapticsStatus: React.FC = () => {

Bluetooth devices are discovered via in-browser WebBluetooth (Chromium browsers only).

The server can send haptic commands when the Client.Haptics GMCP package is active.

Press Escape or use the Emergency Stop button to immediately halt all haptic output.

-

Intensity cap: {(preferences.haptics.intensityCap * 100).toFixed(0)}% | Auto-stop: {preferences.haptics.autoStopTimeout}s

+

Intensity cap: {(hapticsPreferences.intensityCap * 100).toFixed(0)}% | Auto-stop: {hapticsPreferences.autoStopTimeout}s

diff --git a/src/components/MidiStatus.tsx b/src/components/MidiStatus.tsx index fac1876e..f2947d72 100644 --- a/src/components/MidiStatus.tsx +++ b/src/components/MidiStatus.tsx @@ -19,7 +19,7 @@ interface DeviceChangeEvent { } const MidiStatus: React.FC = ({ client }) => { - const preferences = usePreferences(); + const midiPreferences = usePreferences((state) => state.midi); const [midiPackage, setMidiPackage] = useState(null); const [inputDevices, setInputDevices] = useState([]); const [outputDevices, setOutputDevices] = useState([]); @@ -58,7 +58,7 @@ const MidiStatus: React.FC = ({ client }) => { // Load devices when MIDI is enabled const loadDevices = async () => { - if (!preferences.midi.enabled) return; + if (!midiPreferences.enabled) return; // Ensure virtual synthesizer is initialized if (!virtualMidiService.initialized) { @@ -146,7 +146,7 @@ const MidiStatus: React.FC = ({ client }) => { }; useEffect(() => { - if (preferences.midi.enabled) { + if (midiPreferences.enabled) { // Initialize MIDI if not already done if (!midiService.isInitialized && midiPackage) { midiPackage.ensureInitialized().then(() => { @@ -227,7 +227,7 @@ const MidiStatus: React.FC = ({ client }) => { }); setReconnectableDevices({}); } - }, [preferences.midi.enabled, midiService.isInitialized]); + }, [midiPreferences.enabled, midiService.isInitialized]); // Update reconnectable devices when connection state or device lists change useEffect(() => { @@ -363,7 +363,7 @@ const MidiStatus: React.FC = ({ client }) => { return `${noteName}${octave}`; }; - if (!preferences.midi.enabled) { + if (!midiPreferences.enabled) { return (

MIDI is disabled. Enable it in preferences to use MIDI features.

diff --git a/src/components/editor/editorWindow.test.tsx b/src/components/editor/editorWindow.test.tsx index dc386715..a3cb4c77 100644 --- a/src/components/editor/editorWindow.test.tsx +++ b/src/components/editor/editorWindow.test.tsx @@ -96,12 +96,20 @@ vi.mock('@react-aria/live-announcer', () => ({ })); vi.mock('../../stores/preferencesStore', () => ({ - usePreferences: () => ({ - editor: { - accessibilityMode: false, - autocompleteEnabled: true, - }, - }), + usePreferences: ( + selector: (state: { + editor: { + accessibilityMode: boolean; + autocompleteEnabled: boolean; + }; + }) => unknown, + ) => + selector({ + editor: { + accessibilityMode: false, + autocompleteEnabled: true, + }, + }), })); vi.mock('../../editor/monacoLoader', () => ({ diff --git a/src/components/editor/editorWindow.tsx b/src/components/editor/editorWindow.tsx index 3294a550..74309380 100644 --- a/src/components/editor/editorWindow.tsx +++ b/src/components/editor/editorWindow.tsx @@ -110,9 +110,9 @@ function EditorWindow() { const handleEditorBeforeMount = (monaco: Monaco) => { registerMooLanguage(monaco); }; - const prefState = usePreferences(); - const accessibilityMode = prefState.editor.accessibilityMode; - const autocompleteEnabled = prefState.editor.autocompleteEnabled; + const editorPreferences = usePreferences((state) => state.editor); + const accessibilityMode = editorPreferences.accessibilityMode; + const autocompleteEnabled = editorPreferences.autocompleteEnabled; const editorLanguage = getEditorLanguageForSessionType(session.type); const updateMooDiagnostics = React.useCallback((markers: MonacoEditor.IMarkerData[]) => { setMooDiagnosticMarkers(markers); diff --git a/src/components/output.tsx b/src/components/output.tsx index 2a41c0eb..eed676c8 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -114,10 +114,8 @@ class Output extends React.Component { }; } - handlePreferencesChange = () => { - this.setState({ - localEchoActive: usePreferences.getState().general.localEcho, - }); + handlePreferencesChange = (localEchoActive: boolean) => { + this.setState({ localEchoActive }); }; saveOutput = () => { @@ -481,7 +479,10 @@ componentDidUpdate( }; componentDidMount() { - this.unsubscribePrefs = usePreferences.subscribe(this.handlePreferencesChange); + this.unsubscribePrefs = usePreferences.subscribe( + (state) => state.general.localEcho, + this.handlePreferencesChange, + ); this.unsubscribeUserlist = useUserlistStore.subscribe(this.handleUserlistVisibility); this.unsubscribeConnection = useConnectionStore.subscribe(this.handleConnectionStateChange); this.unsubscribeOutputStore = useOutputStore.subscribe(this.handleOutputStoreEntries); diff --git a/src/components/preferences.tsx b/src/components/preferences.tsx index b456790f..b06bd646 100644 --- a/src/components/preferences.tsx +++ b/src/components/preferences.tsx @@ -7,16 +7,17 @@ import Tabs, { type TabProps } from "./tabs"; import AutoLogDialog, { type AutoLogDialogRef } from "./AutoLogDialog"; const GeneralTab: React.FC = () => { - const state = usePreferences(); + const general = usePreferences((state) => state.general); + const setGeneral = usePreferences((state) => state.setGeneral); return (