Skip to content

Commit 449c615

Browse files
committed
feat: Better sync in frontend for mode change
1 parent b264f4e commit 449c615

File tree

4 files changed

+197
-43
lines changed

4 files changed

+197
-43
lines changed

apps/desktop/src/components/Mode.tsx

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,57 @@
1-
import { createSignal } from "solid-js";
1+
import { createEffect, createSignal, onMount } from "solid-js";
22
import Tooltip from "~/components/Tooltip";
33
import { createOptionsQuery } from "~/utils/queries";
44
import { commands } from "~/utils/tauri";
5+
import { trackEvent } from "~/utils/analytics";
6+
import { createStore } from "solid-js/store";
7+
8+
// Create a global store for mode state that all components can access
9+
const [modeState, setModeState] = createStore({
10+
current: "studio" as "instant" | "studio",
11+
initialized: false,
12+
});
13+
14+
// Export this so other components can directly access the current mode
15+
export const getModeState = () => modeState.current;
16+
export const setApplicationMode = (mode: "instant" | "studio") => {
17+
setModeState({ current: mode, initialized: true });
18+
// Also dispatch an event for components that might be listening
19+
window.dispatchEvent(new CustomEvent("cap:mode-change", { detail: mode }));
20+
};
521

622
const Mode = () => {
723
const { options, setOptions } = createOptionsQuery();
824
const [isInfoHovered, setIsInfoHovered] = createSignal(false);
925

26+
// Initialize the mode from options when data is available
27+
createEffect(() => {
28+
if (options.data?.mode) {
29+
if (!modeState.initialized || options.data.mode !== modeState.current) {
30+
console.log("Initializing mode state from options:", options.data.mode);
31+
setModeState({ current: options.data.mode, initialized: true });
32+
}
33+
}
34+
});
35+
36+
// Listen for mode change events
37+
onMount(() => {
38+
const handleModeChange = (e: CustomEvent) => {
39+
console.log("Mode change event received:", e.detail);
40+
};
41+
42+
window.addEventListener(
43+
"cap:mode-change",
44+
handleModeChange as EventListener
45+
);
46+
47+
return () => {
48+
window.removeEventListener(
49+
"cap:mode-change",
50+
handleModeChange as EventListener
51+
);
52+
};
53+
});
54+
1055
const openModeSelectWindow = async () => {
1156
try {
1257
await commands.showWindow("ModeSelect");
@@ -15,6 +60,25 @@ const Mode = () => {
1560
}
1661
};
1762

63+
const handleModeChange = (mode: "instant" | "studio") => {
64+
if (!options.data) return;
65+
if (mode === modeState.current) return;
66+
67+
console.log("Mode changing from", modeState.current, "to", mode);
68+
69+
// Update global state immediately for responsive UI
70+
setApplicationMode(mode);
71+
72+
// Track the mode change event
73+
trackEvent("mode_changed", { from: modeState.current, to: mode });
74+
75+
// Update the backend options while preserving camera/microphone settings
76+
setOptions.mutate({
77+
...options.data,
78+
mode,
79+
});
80+
};
81+
1882
return (
1983
<div class="flex gap-2 relative justify-end items-center p-1.5 rounded-full bg-gray-200 w-fit">
2084
<div
@@ -35,11 +99,10 @@ const Mode = () => {
3599
>
36100
<div
37101
onClick={() => {
38-
if (!options.data) return;
39-
setOptions.mutate({ ...options.data, mode: "instant" });
102+
handleModeChange("instant");
40103
}}
41104
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
42-
options.data?.mode === "instant"
105+
modeState.current === "instant"
43106
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
44107
: "bg-gray-200 hover:bg-[--gray-300]"
45108
}`}
@@ -58,11 +121,10 @@ const Mode = () => {
58121
>
59122
<div
60123
onClick={() => {
61-
if (!options.data) return;
62-
setOptions.mutate({ ...options.data, mode: "studio" });
124+
handleModeChange("studio");
63125
}}
64126
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
65-
options.data?.mode === "studio"
127+
modeState.current === "studio"
66128
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
67129
: "bg-gray-200 hover:bg-[--gray-300]"
68130
}`}
@@ -76,11 +138,10 @@ const Mode = () => {
76138
<>
77139
<div
78140
onClick={() => {
79-
if (!options.data) return;
80-
setOptions.mutate({ ...options.data, mode: "instant" });
141+
handleModeChange("instant");
81142
}}
82143
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
83-
options.data?.mode === "instant"
144+
modeState.current === "instant"
84145
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
85146
: "bg-gray-200 hover:bg-[--gray-300]"
86147
}`}
@@ -90,11 +151,10 @@ const Mode = () => {
90151

91152
<div
92153
onClick={() => {
93-
if (!options.data) return;
94-
setOptions.mutate({ ...options.data, mode: "studio" });
154+
handleModeChange("studio");
95155
}}
96156
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
97-
options.data?.mode === "studio"
157+
modeState.current === "studio"
98158
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
99159
: "bg-gray-200 hover:bg-[--gray-300]"
100160
}`}

apps/desktop/src/components/ModeSelect.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { JSX } from "solid-js";
1+
import { JSX, createEffect, createMemo } from "solid-js";
22
import { createOptionsQuery } from "~/utils/queries";
33
import { RecordingMode } from "~/utils/tauri";
44
import InstantModeDark from "../assets/illustrations/instant-mode-dark.png";
55
import InstantModeLight from "../assets/illustrations/instant-mode-light.png";
66
import StudioModeDark from "../assets/illustrations/studio-mode-dark.png";
77
import StudioModeLight from "../assets/illustrations/studio-mode-light.png";
8+
import { getModeState, setApplicationMode } from "./Mode";
89

910
interface ModeOptionProps {
1011
mode: RecordingMode;
@@ -63,11 +64,32 @@ interface ModeSelectProps {
6364
const ModeSelect = (props: ModeSelectProps) => {
6465
const { options, setOptions } = createOptionsQuery();
6566

67+
// Use createMemo to make the mode state reactive
68+
const currentGlobalMode = createMemo(() => getModeState());
69+
70+
// If there's an initialMode prop, we should use that
71+
const selectedMode = createMemo(() =>
72+
props.initialMode ? props.initialMode : currentGlobalMode()
73+
);
74+
75+
// For debugging
76+
createEffect(() => {
77+
console.log("Current mode in ModeSelect:", selectedMode());
78+
});
79+
6680
const handleModeChange = (mode: RecordingMode) => {
6781
if (props.onModeChange) {
6882
props.onModeChange(mode);
6983
} else if (options.data) {
70-
setOptions.mutate({ ...options.data, mode });
84+
// Update global state for immediate UI response
85+
setApplicationMode(mode);
86+
87+
// Keep existing settings while changing the mode
88+
// This keeps camera and microphone settings as they were
89+
setOptions.mutate({
90+
...options.data,
91+
mode,
92+
});
7193
}
7294
};
7395

@@ -102,11 +124,7 @@ const ModeSelect = (props: ModeSelectProps) => {
102124
darkimg={option.darkimg}
103125
lightimg={option.lightimg}
104126
icon={option.icon}
105-
isSelected={
106-
props.initialMode
107-
? props.initialMode === option.mode
108-
: options.data?.mode === option.mode
109-
}
127+
isSelected={selectedMode() === option.mode}
110128
onSelect={handleModeChange}
111129
/>
112130
))}

apps/desktop/src/routes/(window-chrome)/(main).tsx

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,72 @@ import {
5050
topLeftAnimateClasses,
5151
topRightAnimateClasses,
5252
} from "../editor/ui";
53-
import Mode from "~/components/Mode";
53+
import Mode, { getModeState, setApplicationMode } from "~/components/Mode";
5454

5555
export default function () {
5656
const { options, setOptions } = createOptionsQuery();
5757
const currentRecording = createCurrentRecordingQuery();
58+
const queryClient = useQueryClient();
5859

5960
const isRecording = () => !!currentRecording.data;
6061

62+
// Initialize application mode from options when available
63+
createEffect(() => {
64+
if (options.data?.mode) {
65+
// Set the global application mode to match the options
66+
setApplicationMode(options.data.mode);
67+
}
68+
});
69+
70+
// Use getModeState for direct access to the current mode
71+
// This will be more reliable than tracking local state
72+
const currentMode = createMemo(() => {
73+
// Get the mode from our global store
74+
return getModeState();
75+
});
76+
77+
// Listen for mode changes via event for components that need to respond
78+
onMount(() => {
79+
const handleModeChange = (e: CustomEvent) => {
80+
// Force re-rendering by notifying the reactive system
81+
// This isn't strictly necessary since currentMode() uses getModeState()
82+
// But it ensures the UI updates in case there are any reactivity issues
83+
console.log(`Mode changed to: ${e.detail}`);
84+
};
85+
86+
window.addEventListener(
87+
"cap:mode-change",
88+
handleModeChange as EventListener
89+
);
90+
onCleanup(() =>
91+
window.removeEventListener(
92+
"cap:mode-change",
93+
handleModeChange as EventListener
94+
)
95+
);
96+
});
97+
98+
// Track if we've already handled camera initialization to prevent reopening on mode changes
99+
const [cameraInitialized, setCameraInitialized] = createSignal(false);
100+
101+
// Handle camera window initialization separately from mode changes
102+
createEffect(() => {
103+
if (!options.data || cameraInitialized()) return;
104+
105+
// Only initialize camera if a valid camera is selected
106+
if (options.data.cameraLabel && options.data.cameraLabel !== "No Camera") {
107+
commands.isCameraWindowOpen().then((cameraWindowActive) => {
108+
if (!cameraWindowActive) {
109+
console.log("Initializing camera window");
110+
setCameraInitialized(true);
111+
}
112+
});
113+
} else {
114+
// Mark as initialized even if no camera is selected
115+
setCameraInitialized(true);
116+
}
117+
});
118+
61119
const toggleRecording = createMutation(() => ({
62120
mutationFn: async () => {
63121
if (!isRecording()) {
@@ -77,17 +135,6 @@ export default function () {
77135
const [initialize] = createResource(async () => {
78136
const version = await getVersion();
79137

80-
if (options.data?.cameraLabel && options.data.cameraLabel !== "No Camera") {
81-
const cameraWindowActive = await commands.isCameraWindowOpen();
82-
83-
if (!cameraWindowActive) {
84-
console.log("cameraWindow not found");
85-
setOptions.mutate({
86-
...options.data,
87-
});
88-
}
89-
}
90-
91138
// Enforce window size with multiple safeguards
92139
const currentWindow = getCurrentWindow();
93140
const MAIN_WINDOW_SIZE = { width: 300, height: 360 };
@@ -247,11 +294,14 @@ export default function () {
247294
"Stop Recording"
248295
) : (
249296
<>
250-
{options.data?.mode === "instant" ? (
297+
<Show
298+
when={currentMode() === "instant"}
299+
fallback={
300+
<IconCapFilmCut class="w-[0.8rem] h-[0.8rem] mr-2 -mt-[1.5px]" />
301+
}
302+
>
251303
<IconCapInstant class="w-[0.8rem] h-[0.8rem] mr-1.5" />
252-
) : (
253-
<IconCapFilmCut class="w-[0.8rem] h-[0.8rem] mr-2 -mt-[1.5px]" />
254-
)}
304+
</Show>
255305
Start Recording
256306
</>
257307
)}
@@ -622,12 +672,15 @@ function CameraSelect(props: {
622672

623673
setLoading(true);
624674
await props.setOptions
625-
.mutateAsync({ ...props.options, cameraLabel })
675+
.mutateAsync({
676+
...props.options,
677+
cameraLabel: cameraLabel === "No Camera" ? null : cameraLabel,
678+
})
626679
.finally(() => setLoading(false));
627680

628681
trackEvent("camera_selected", {
629682
camera_name: cameraLabel,
630-
enabled: !!cameraLabel,
683+
enabled: !!cameraLabel && cameraLabel !== "No Camera",
631684
});
632685
};
633686

@@ -737,15 +790,18 @@ function MicrophoneSelect(props: {
737790
const handleMicrophoneChange = async (item: Option | null) => {
738791
if (!item || !props.options) return;
739792

793+
// If "No Microphone" is selected, set audioInputName to null
794+
const audioInputName = item.deviceId !== "" ? item.name : null;
795+
740796
await props.setOptions.mutateAsync({
741797
...props.options,
742-
audioInputName: item.deviceId !== "" ? item.name : null,
798+
audioInputName,
743799
});
744-
if (!item.deviceId) setDbs();
800+
if (!audioInputName) setDbs();
745801

746802
trackEvent("microphone_selected", {
747-
microphone_name: item.deviceId !== "" ? item.name : null,
748-
enabled: item.deviceId !== "",
803+
microphone_name: audioInputName,
804+
enabled: !!audioInputName,
749805
});
750806
};
751807

0 commit comments

Comments
 (0)