Skip to content

Commit

Permalink
feat: Better sync in frontend for mode change
Browse files Browse the repository at this point in the history
  • Loading branch information
richiemcilroy committed Feb 27, 2025
1 parent b264f4e commit 449c615
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 43 deletions.
86 changes: 73 additions & 13 deletions apps/desktop/src/components/Mode.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,57 @@
import { createSignal } from "solid-js";
import { createEffect, createSignal, onMount } from "solid-js";
import Tooltip from "~/components/Tooltip";
import { createOptionsQuery } from "~/utils/queries";
import { commands } from "~/utils/tauri";
import { trackEvent } from "~/utils/analytics";
import { createStore } from "solid-js/store";

// Create a global store for mode state that all components can access
const [modeState, setModeState] = createStore({
current: "studio" as "instant" | "studio",
initialized: false,
});

// Export this so other components can directly access the current mode
export const getModeState = () => modeState.current;
export const setApplicationMode = (mode: "instant" | "studio") => {
setModeState({ current: mode, initialized: true });
// Also dispatch an event for components that might be listening
window.dispatchEvent(new CustomEvent("cap:mode-change", { detail: mode }));
};

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

// Initialize the mode from options when data is available
createEffect(() => {
if (options.data?.mode) {
if (!modeState.initialized || options.data.mode !== modeState.current) {
console.log("Initializing mode state from options:", options.data.mode);
setModeState({ current: options.data.mode, initialized: true });
}
}
});

// Listen for mode change events
onMount(() => {
const handleModeChange = (e: CustomEvent) => {
console.log("Mode change event received:", e.detail);
};

window.addEventListener(
"cap:mode-change",
handleModeChange as EventListener
);

return () => {
window.removeEventListener(
"cap:mode-change",
handleModeChange as EventListener
);
};
});

const openModeSelectWindow = async () => {
try {
await commands.showWindow("ModeSelect");
Expand All @@ -15,6 +60,25 @@ const Mode = () => {
}
};

const handleModeChange = (mode: "instant" | "studio") => {
if (!options.data) return;
if (mode === modeState.current) return;

console.log("Mode changing from", modeState.current, "to", mode);

// Update global state immediately for responsive UI
setApplicationMode(mode);

// Track the mode change event
trackEvent("mode_changed", { from: modeState.current, to: mode });

// Update the backend options while preserving camera/microphone settings
setOptions.mutate({
...options.data,
mode,
});
};

return (
<div class="flex gap-2 relative justify-end items-center p-1.5 rounded-full bg-gray-200 w-fit">
<div
Expand All @@ -35,11 +99,10 @@ const Mode = () => {
>
<div
onClick={() => {
if (!options.data) return;
setOptions.mutate({ ...options.data, mode: "instant" });
handleModeChange("instant");
}}
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
options.data?.mode === "instant"
modeState.current === "instant"
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
: "bg-gray-200 hover:bg-[--gray-300]"
}`}
Expand All @@ -58,11 +121,10 @@ const Mode = () => {
>
<div
onClick={() => {
if (!options.data) return;
setOptions.mutate({ ...options.data, mode: "studio" });
handleModeChange("studio");
}}
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
options.data?.mode === "studio"
modeState.current === "studio"
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
: "bg-gray-200 hover:bg-[--gray-300]"
}`}
Expand All @@ -76,11 +138,10 @@ const Mode = () => {
<>
<div
onClick={() => {
if (!options.data) return;
setOptions.mutate({ ...options.data, mode: "instant" });
handleModeChange("instant");
}}
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
options.data?.mode === "instant"
modeState.current === "instant"
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
: "bg-gray-200 hover:bg-[--gray-300]"
}`}
Expand All @@ -90,11 +151,10 @@ const Mode = () => {

<div
onClick={() => {
if (!options.data) return;
setOptions.mutate({ ...options.data, mode: "studio" });
handleModeChange("studio");
}}
class={`flex justify-center items-center transition-all duration-200 rounded-full size-7 hover:cursor-pointer ${
options.data?.mode === "studio"
modeState.current === "studio"
? "ring-2 ring-offset-1 ring-offset-gray-50 bg-gray-300 hover:bg-[--gray-300] ring-[--blue-300]"
: "bg-gray-200 hover:bg-[--gray-300]"
}`}
Expand Down
32 changes: 25 additions & 7 deletions apps/desktop/src/components/ModeSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { JSX } from "solid-js";
import { JSX, createEffect, createMemo } from "solid-js";
import { createOptionsQuery } from "~/utils/queries";
import { RecordingMode } from "~/utils/tauri";
import InstantModeDark from "../assets/illustrations/instant-mode-dark.png";
import InstantModeLight from "../assets/illustrations/instant-mode-light.png";
import StudioModeDark from "../assets/illustrations/studio-mode-dark.png";
import StudioModeLight from "../assets/illustrations/studio-mode-light.png";
import { getModeState, setApplicationMode } from "./Mode";

interface ModeOptionProps {
mode: RecordingMode;
Expand Down Expand Up @@ -63,11 +64,32 @@ interface ModeSelectProps {
const ModeSelect = (props: ModeSelectProps) => {
const { options, setOptions } = createOptionsQuery();

// Use createMemo to make the mode state reactive
const currentGlobalMode = createMemo(() => getModeState());

// If there's an initialMode prop, we should use that
const selectedMode = createMemo(() =>
props.initialMode ? props.initialMode : currentGlobalMode()
);

// For debugging
createEffect(() => {
console.log("Current mode in ModeSelect:", selectedMode());
});

const handleModeChange = (mode: RecordingMode) => {
if (props.onModeChange) {
props.onModeChange(mode);
} else if (options.data) {
setOptions.mutate({ ...options.data, mode });
// Update global state for immediate UI response
setApplicationMode(mode);

// Keep existing settings while changing the mode
// This keeps camera and microphone settings as they were
setOptions.mutate({
...options.data,
mode,
});
}
};

Expand Down Expand Up @@ -102,11 +124,7 @@ const ModeSelect = (props: ModeSelectProps) => {
darkimg={option.darkimg}
lightimg={option.lightimg}
icon={option.icon}
isSelected={
props.initialMode
? props.initialMode === option.mode
: options.data?.mode === option.mode
}
isSelected={selectedMode() === option.mode}
onSelect={handleModeChange}
/>
))}
Expand Down
100 changes: 78 additions & 22 deletions apps/desktop/src/routes/(window-chrome)/(main).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,72 @@ import {
topLeftAnimateClasses,
topRightAnimateClasses,
} from "../editor/ui";
import Mode from "~/components/Mode";
import Mode, { getModeState, setApplicationMode } from "~/components/Mode";

export default function () {
const { options, setOptions } = createOptionsQuery();
const currentRecording = createCurrentRecordingQuery();
const queryClient = useQueryClient();

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

// Initialize application mode from options when available
createEffect(() => {
if (options.data?.mode) {
// Set the global application mode to match the options
setApplicationMode(options.data.mode);
}
});

// Use getModeState for direct access to the current mode
// This will be more reliable than tracking local state
const currentMode = createMemo(() => {
// Get the mode from our global store
return getModeState();
});

// Listen for mode changes via event for components that need to respond
onMount(() => {
const handleModeChange = (e: CustomEvent) => {
// Force re-rendering by notifying the reactive system
// This isn't strictly necessary since currentMode() uses getModeState()
// But it ensures the UI updates in case there are any reactivity issues
console.log(`Mode changed to: ${e.detail}`);
};

window.addEventListener(
"cap:mode-change",
handleModeChange as EventListener
);
onCleanup(() =>
window.removeEventListener(
"cap:mode-change",
handleModeChange as EventListener
)
);
});

// Track if we've already handled camera initialization to prevent reopening on mode changes
const [cameraInitialized, setCameraInitialized] = createSignal(false);

// Handle camera window initialization separately from mode changes
createEffect(() => {
if (!options.data || cameraInitialized()) return;

// Only initialize camera if a valid camera is selected
if (options.data.cameraLabel && options.data.cameraLabel !== "No Camera") {
commands.isCameraWindowOpen().then((cameraWindowActive) => {
if (!cameraWindowActive) {
console.log("Initializing camera window");
setCameraInitialized(true);
}
});
} else {
// Mark as initialized even if no camera is selected
setCameraInitialized(true);
}
});

const toggleRecording = createMutation(() => ({
mutationFn: async () => {
if (!isRecording()) {
Expand All @@ -77,17 +135,6 @@ export default function () {
const [initialize] = createResource(async () => {
const version = await getVersion();

if (options.data?.cameraLabel && options.data.cameraLabel !== "No Camera") {
const cameraWindowActive = await commands.isCameraWindowOpen();

if (!cameraWindowActive) {
console.log("cameraWindow not found");
setOptions.mutate({
...options.data,
});
}
}

// Enforce window size with multiple safeguards
const currentWindow = getCurrentWindow();
const MAIN_WINDOW_SIZE = { width: 300, height: 360 };
Expand Down Expand Up @@ -247,11 +294,14 @@ export default function () {
"Stop Recording"
) : (
<>
{options.data?.mode === "instant" ? (
<Show
when={currentMode() === "instant"}
fallback={
<IconCapFilmCut class="w-[0.8rem] h-[0.8rem] mr-2 -mt-[1.5px]" />
}
>
<IconCapInstant class="w-[0.8rem] h-[0.8rem] mr-1.5" />
) : (
<IconCapFilmCut class="w-[0.8rem] h-[0.8rem] mr-2 -mt-[1.5px]" />
)}
</Show>
Start Recording
</>
)}
Expand Down Expand Up @@ -622,12 +672,15 @@ function CameraSelect(props: {

setLoading(true);
await props.setOptions
.mutateAsync({ ...props.options, cameraLabel })
.mutateAsync({
...props.options,
cameraLabel: cameraLabel === "No Camera" ? null : cameraLabel,
})
.finally(() => setLoading(false));

trackEvent("camera_selected", {
camera_name: cameraLabel,
enabled: !!cameraLabel,
enabled: !!cameraLabel && cameraLabel !== "No Camera",
});
};

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

// If "No Microphone" is selected, set audioInputName to null
const audioInputName = item.deviceId !== "" ? item.name : null;

await props.setOptions.mutateAsync({
...props.options,
audioInputName: item.deviceId !== "" ? item.name : null,
audioInputName,
});
if (!item.deviceId) setDbs();
if (!audioInputName) setDbs();

trackEvent("microphone_selected", {
microphone_name: item.deviceId !== "" ? item.name : null,
enabled: item.deviceId !== "",
microphone_name: audioInputName,
enabled: !!audioInputName,
});
};

Expand Down
Loading

0 comments on commit 449c615

Please sign in to comment.